mirror of
https://github.com/google/nomulus.git
synced 2025-05-30 01:10:14 +02:00
Refactor common email sending utility
The main thrust of this is to create a common POJO that contains email content in a simple way, then have one class that converts that to an email and sends it. Any class that uses email should only have to deal with creating that POJO. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=237883643
This commit is contained in:
parent
9823ee7fcf
commit
50e0a9b532
35 changed files with 773 additions and 614 deletions
|
@ -15,6 +15,7 @@
|
|||
package google.registry.config;
|
||||
|
||||
import static com.google.common.base.Suppliers.memoize;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.config.ConfigUtils.makeUrl;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
@ -41,6 +42,8 @@ import javax.annotation.Nullable;
|
|||
import javax.inject.Named;
|
||||
import javax.inject.Qualifier;
|
||||
import javax.inject.Singleton;
|
||||
import javax.mail.internet.AddressException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import org.joda.time.DateTimeConstants;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
|
@ -510,8 +513,8 @@ public final class RegistryConfig {
|
|||
*/
|
||||
@Provides
|
||||
@Config("gSuiteOutgoingEmailAddress")
|
||||
public static String provideGSuiteOutgoingEmailAddress(RegistryConfigSettings config) {
|
||||
return config.gSuite.outgoingEmailAddress;
|
||||
public static InternetAddress provideGSuiteOutgoingEmailAddress(RegistryConfigSettings config) {
|
||||
return parseEmailAddress(config.gSuite.outgoingEmailAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -697,9 +700,11 @@ public final class RegistryConfig {
|
|||
*/
|
||||
@Provides
|
||||
@Config("invoiceEmailRecipients")
|
||||
public static ImmutableList<String> provideInvoiceEmailRecipients(
|
||||
public static ImmutableList<InternetAddress> provideInvoiceEmailRecipients(
|
||||
RegistryConfigSettings config) {
|
||||
return ImmutableList.copyOf(config.billing.invoiceEmailRecipients);
|
||||
return config.billing.invoiceEmailRecipients.stream()
|
||||
.map(RegistryConfig::parseEmailAddress)
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -863,14 +868,13 @@ public final class RegistryConfig {
|
|||
* <p>This allows us to easily verify the success or failure of periodic tasks by passively
|
||||
* checking e-mail.
|
||||
*
|
||||
* @see google.registry.reporting.icann.ReportingEmailUtils
|
||||
* @see google.registry.reporting.billing.BillingEmailUtils
|
||||
* @see google.registry.reporting.spec11.Spec11EmailUtils
|
||||
*/
|
||||
@Provides
|
||||
@Config("alertRecipientEmailAddress")
|
||||
public static String provideAlertRecipientEmailAddress(RegistryConfigSettings config) {
|
||||
return config.misc.alertRecipientEmailAddress;
|
||||
public static InternetAddress provideAlertRecipientEmailAddress(RegistryConfigSettings config) {
|
||||
return parseEmailAddress(config.misc.alertRecipientEmailAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -880,8 +884,8 @@ public final class RegistryConfig {
|
|||
*/
|
||||
@Provides
|
||||
@Config("spec11ReplyToEmailAddress")
|
||||
public static String provideSpec11ReplyToEmailAddress(RegistryConfigSettings config) {
|
||||
return config.misc.spec11ReplyToEmailAddress;
|
||||
public static InternetAddress provideSpec11ReplyToEmailAddress(RegistryConfigSettings config) {
|
||||
return parseEmailAddress(config.misc.spec11ReplyToEmailAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -931,7 +935,7 @@ public final class RegistryConfig {
|
|||
/**
|
||||
* Number of times to retry a GAE operation when {@code TransientFailureException} is thrown.
|
||||
*
|
||||
* <p>The number of milliseconds it'll sleep before giving up is {@code 2^n - 2}.
|
||||
* <p>The number of milliseconds it'll sleep before giving up is {@code (2^n - 2) * 100}.
|
||||
*
|
||||
* <p>Note that this uses {@code @Named} instead of {@code @Config} so that it can be used from
|
||||
* the low-level util package, which cannot have a dependency on the config package.
|
||||
|
@ -940,8 +944,8 @@ public final class RegistryConfig {
|
|||
*/
|
||||
@Provides
|
||||
@Named("transientFailureRetries")
|
||||
public static int provideTransientFailureRetries() {
|
||||
return 12; // Four seconds.
|
||||
public static int provideTransientFailureRetries(RegistryConfigSettings config) {
|
||||
return config.misc.transientFailureRetries;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1489,8 +1493,8 @@ public final class RegistryConfig {
|
|||
}
|
||||
|
||||
/** Returns the email address that outgoing emails from the app are sent from. */
|
||||
public static String getGSuiteOutgoingEmailAddress() {
|
||||
return CONFIG_SETTINGS.get().gSuite.outgoingEmailAddress;
|
||||
public static InternetAddress getGSuiteOutgoingEmailAddress() {
|
||||
return parseEmailAddress(CONFIG_SETTINGS.get().gSuite.outgoingEmailAddress);
|
||||
}
|
||||
|
||||
/** Returns the display name that outgoing emails from the app are sent from. */
|
||||
|
@ -1548,5 +1552,13 @@ public final class RegistryConfig {
|
|||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private static InternetAddress parseEmailAddress(String email) {
|
||||
try {
|
||||
return new InternetAddress(email);
|
||||
} catch (AddressException e) {
|
||||
throw new IllegalArgumentException(String.format("Could not parse email address %s.", email));
|
||||
}
|
||||
}
|
||||
|
||||
private RegistryConfig() {}
|
||||
}
|
||||
|
|
|
@ -169,6 +169,7 @@ public class RegistryConfigSettings {
|
|||
public String alertRecipientEmailAddress;
|
||||
public String spec11ReplyToEmailAddress;
|
||||
public int asyncDeleteDelaySeconds;
|
||||
public int transientFailureRetries;
|
||||
}
|
||||
|
||||
/** Configuration for keyrings (used to store secrets outside of source). */
|
||||
|
|
|
@ -361,6 +361,10 @@ misc:
|
|||
# hosts from being used on domains.
|
||||
asyncDeleteDelaySeconds: 90
|
||||
|
||||
# Number of times to retry a GAE operation when a transient exception is thrown.
|
||||
# The number of milliseconds it'll sleep before giving up is (2^n - 2) * 100.
|
||||
transientFailureRetries: 12
|
||||
|
||||
beam:
|
||||
# The default zone to run Apache Beam (Cloud Dataflow) jobs in.
|
||||
defaultJobZone: us-east1-c
|
||||
|
|
|
@ -27,3 +27,7 @@ caching:
|
|||
# tests
|
||||
gSuite:
|
||||
supportGroupEmailAddress:
|
||||
|
||||
misc:
|
||||
# We would rather have failures than timeouts, so reduce the number of retries
|
||||
transientFailureRetries: 3
|
||||
|
|
|
@ -20,23 +20,17 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||
import com.google.appengine.tools.cloudstorage.GcsFilename;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.gcs.GcsUtils;
|
||||
import google.registry.reporting.billing.BillingModule.InvoiceDirectoryPrefix;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.EmailMessage.Attachment;
|
||||
import google.registry.util.SendEmailService;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.BodyPart;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.Message.RecipientType;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.Multipart;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeBodyPart;
|
||||
import javax.mail.internet.MimeMultipart;
|
||||
import org.joda.time.YearMonth;
|
||||
|
||||
/** Utility functions for sending emails involving monthly invoices. */
|
||||
|
@ -44,27 +38,25 @@ class BillingEmailUtils {
|
|||
|
||||
private final SendEmailService emailService;
|
||||
private final YearMonth yearMonth;
|
||||
private final String outgoingEmailAddress;
|
||||
private final String alertRecipientAddress;
|
||||
private final ImmutableList<String> invoiceEmailRecipients;
|
||||
private final InternetAddress outgoingEmailAddress;
|
||||
private final InternetAddress alertRecipientAddress;
|
||||
private final ImmutableList<InternetAddress> invoiceEmailRecipients;
|
||||
private final String billingBucket;
|
||||
private final String invoiceFilePrefix;
|
||||
private final String invoiceDirectoryPrefix;
|
||||
private final GcsUtils gcsUtils;
|
||||
private final Retrier retrier;
|
||||
|
||||
@Inject
|
||||
BillingEmailUtils(
|
||||
SendEmailService emailService,
|
||||
YearMonth yearMonth,
|
||||
@Config("gSuiteOutgoingEmailAddress") String outgoingEmailAddress,
|
||||
@Config("alertRecipientEmailAddress") String alertRecipientAddress,
|
||||
@Config("invoiceEmailRecipients") ImmutableList<String> invoiceEmailRecipients,
|
||||
@Config("gSuiteOutgoingEmailAddress") InternetAddress outgoingEmailAddress,
|
||||
@Config("alertRecipientEmailAddress") InternetAddress alertRecipientAddress,
|
||||
@Config("invoiceEmailRecipients") ImmutableList<InternetAddress> invoiceEmailRecipients,
|
||||
@Config("billingBucket") String billingBucket,
|
||||
@Config("invoiceFilePrefix") String invoiceFilePrefix,
|
||||
@InvoiceDirectoryPrefix String invoiceDirectoryPrefix,
|
||||
GcsUtils gcsUtils,
|
||||
Retrier retrier) {
|
||||
GcsUtils gcsUtils) {
|
||||
this.emailService = emailService;
|
||||
this.yearMonth = yearMonth;
|
||||
this.outgoingEmailAddress = outgoingEmailAddress;
|
||||
|
@ -74,50 +66,34 @@ class BillingEmailUtils {
|
|||
this.invoiceFilePrefix = invoiceFilePrefix;
|
||||
this.invoiceDirectoryPrefix = invoiceDirectoryPrefix;
|
||||
this.gcsUtils = gcsUtils;
|
||||
this.retrier = retrier;
|
||||
}
|
||||
|
||||
/** Sends an e-mail to all expected recipients with an attached overall invoice from GCS. */
|
||||
void emailOverallInvoice() {
|
||||
try {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
String invoiceFile =
|
||||
String.format("%s-%s.csv", invoiceFilePrefix, yearMonth);
|
||||
GcsFilename invoiceFilename =
|
||||
new GcsFilename(billingBucket, invoiceDirectoryPrefix + invoiceFile);
|
||||
try (InputStream in = gcsUtils.openInputStream(invoiceFilename)) {
|
||||
Message msg = emailService.createMessage();
|
||||
msg.setFrom(new InternetAddress(outgoingEmailAddress));
|
||||
for (String recipient : invoiceEmailRecipients) {
|
||||
msg.addRecipient(RecipientType.TO, new InternetAddress(recipient));
|
||||
}
|
||||
msg.setSubject(
|
||||
String.format("Domain Registry invoice data %s", yearMonth));
|
||||
Multipart multipart = new MimeMultipart();
|
||||
BodyPart textPart = new MimeBodyPart();
|
||||
textPart.setText(
|
||||
String.format(
|
||||
"Attached is the %s invoice for the domain registry.", yearMonth));
|
||||
multipart.addBodyPart(textPart);
|
||||
BodyPart invoicePart = new MimeBodyPart();
|
||||
String invoiceData = CharStreams.toString(new InputStreamReader(in, UTF_8));
|
||||
invoicePart.setContent(invoiceData, "text/csv; charset=utf-8");
|
||||
invoicePart.setFileName(invoiceFile);
|
||||
multipart.addBodyPart(invoicePart);
|
||||
msg.setContent(multipart);
|
||||
msg.saveChanges();
|
||||
emailService.sendMessage(msg);
|
||||
}
|
||||
},
|
||||
IOException.class,
|
||||
MessagingException.class);
|
||||
String invoiceFile = String.format("%s-%s.csv", invoiceFilePrefix, yearMonth);
|
||||
GcsFilename invoiceFilename =
|
||||
new GcsFilename(billingBucket, invoiceDirectoryPrefix + invoiceFile);
|
||||
try (InputStream in = gcsUtils.openInputStream(invoiceFilename)) {
|
||||
emailService.sendEmail(
|
||||
EmailMessage.newBuilder()
|
||||
.setSubject(String.format("Domain Registry invoice data %s", yearMonth))
|
||||
.setBody(
|
||||
String.format("Attached is the %s invoice for the domain registry.", yearMonth))
|
||||
.setFrom(outgoingEmailAddress)
|
||||
.setRecipients(invoiceEmailRecipients)
|
||||
.setAttachment(
|
||||
Attachment.newBuilder()
|
||||
.setContent(CharStreams.toString(new InputStreamReader(in, UTF_8)))
|
||||
.setContentType(MediaType.CSV_UTF_8)
|
||||
.setFilename(invoiceFile)
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Strip one layer, because callWithRetry wraps in a RuntimeException
|
||||
sendAlertEmail(
|
||||
String.format(
|
||||
"Emailing invoice failed due to %s",
|
||||
getRootCause(e).getMessage()));
|
||||
String.format("Emailing invoice failed due to %s", getRootCause(e).getMessage()));
|
||||
throw new RuntimeException("Emailing invoice failed", e);
|
||||
}
|
||||
}
|
||||
|
@ -125,17 +101,13 @@ class BillingEmailUtils {
|
|||
/** Sends an e-mail to the provided alert e-mail address indicating a billing failure. */
|
||||
void sendAlertEmail(String body) {
|
||||
try {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
Message msg = emailService.createMessage();
|
||||
msg.setFrom(new InternetAddress(outgoingEmailAddress));
|
||||
msg.addRecipient(RecipientType.TO, new InternetAddress(alertRecipientAddress));
|
||||
msg.setSubject(String.format("Billing Pipeline Alert: %s", yearMonth));
|
||||
msg.setText(body);
|
||||
emailService.sendMessage(msg);
|
||||
return null;
|
||||
},
|
||||
MessagingException.class);
|
||||
emailService.sendEmail(
|
||||
EmailMessage.newBuilder()
|
||||
.setSubject(String.format("Billing Pipeline Alert: %s", yearMonth))
|
||||
.setBody(body)
|
||||
.addRecipient(alertRecipientAddress)
|
||||
.setFrom(outgoingEmailAddress)
|
||||
.build());
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("The alert e-mail system failed.", e);
|
||||
}
|
||||
|
|
|
@ -30,14 +30,18 @@ import com.google.common.collect.ImmutableSet;
|
|||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.bigquery.BigqueryJobFailureException;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.reporting.icann.IcannReportingModule.ReportType;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SendEmailService;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import org.joda.time.Duration;
|
||||
import org.joda.time.YearMonth;
|
||||
import org.joda.time.format.DateTimeFormat;
|
||||
|
@ -79,7 +83,10 @@ public final class IcannReportingStagingAction implements Runnable {
|
|||
@Inject IcannReportingStager stager;
|
||||
@Inject Retrier retrier;
|
||||
@Inject Response response;
|
||||
@Inject ReportingEmailUtils emailUtils;
|
||||
@Inject @Config("gSuiteOutgoingEmailAddress") InternetAddress sender;
|
||||
@Inject @Config("alertRecipientEmailAddress") InternetAddress recipient;
|
||||
@Inject SendEmailService emailService;
|
||||
|
||||
@Inject IcannReportingStagingAction() {}
|
||||
|
||||
@Override
|
||||
|
@ -96,11 +103,16 @@ public final class IcannReportingStagingAction implements Runnable {
|
|||
stager.createAndUploadManifest(subdir, manifestedFiles);
|
||||
|
||||
logger.atInfo().log("Completed staging %d report files.", manifestedFiles.size());
|
||||
emailUtils.emailResults(
|
||||
"ICANN Monthly report staging summary [SUCCESS]",
|
||||
String.format(
|
||||
"Completed staging the following %d ICANN reports:\n%s",
|
||||
manifestedFiles.size(), Joiner.on('\n').join(manifestedFiles)));
|
||||
emailService.sendEmail(
|
||||
EmailMessage.newBuilder()
|
||||
.setSubject("ICANN Monthly report staging summary [SUCCESS]")
|
||||
.setBody(
|
||||
String.format(
|
||||
"Completed staging the following %d ICANN reports:\n%s",
|
||||
manifestedFiles.size(), Joiner.on('\n').join(manifestedFiles)))
|
||||
.addRecipient(recipient)
|
||||
.setFrom(sender)
|
||||
.build());
|
||||
|
||||
response.setStatus(SC_OK);
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
|
@ -117,11 +129,13 @@ public final class IcannReportingStagingAction implements Runnable {
|
|||
},
|
||||
BigqueryJobFailureException.class);
|
||||
} catch (Throwable e) {
|
||||
emailUtils.emailResults(
|
||||
"ICANN Monthly report staging summary [FAILURE]",
|
||||
String.format(
|
||||
"Staging failed due to %s, check logs for more details.",
|
||||
getRootCause(e).toString()));
|
||||
emailService.sendEmail(
|
||||
EmailMessage.create(
|
||||
"ICANN Monthly report staging summary [FAILURE]",
|
||||
String.format(
|
||||
"Staging failed due to %s, check logs for more details.", getRootCause(e)),
|
||||
recipient,
|
||||
sender));
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
response.setPayload(
|
||||
|
|
|
@ -34,11 +34,14 @@ import google.registry.request.Action;
|
|||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SendEmailService;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
/**
|
||||
* Action that uploads the monthly activity/transactions reports from GCS to ICANN via an HTTP PUT.
|
||||
|
@ -74,8 +77,9 @@ public final class IcannReportingUploadAction implements Runnable {
|
|||
@Inject IcannHttpReporter icannReporter;
|
||||
@Inject Retrier retrier;
|
||||
@Inject Response response;
|
||||
@Inject ReportingEmailUtils emailUtils;
|
||||
|
||||
@Inject @Config("gSuiteOutgoingEmailAddress") InternetAddress sender;
|
||||
@Inject @Config("alertRecipientEmailAddress") InternetAddress recipient;
|
||||
@Inject SendEmailService emailService;
|
||||
@Inject
|
||||
IcannReportingUploadAction() {}
|
||||
|
||||
|
@ -112,19 +116,18 @@ public final class IcannReportingUploadAction implements Runnable {
|
|||
}
|
||||
|
||||
private void emailUploadResults(ImmutableMap<String, Boolean> reportSummary) {
|
||||
emailUtils.emailResults(
|
||||
String.format(
|
||||
"ICANN Monthly report upload summary: %d/%d succeeded",
|
||||
reportSummary.values().stream().filter((b) -> b).count(), reportSummary.size()),
|
||||
String subject = String.format(
|
||||
"ICANN Monthly report upload summary: %d/%d succeeded",
|
||||
reportSummary.values().stream().filter((b) -> b).count(), reportSummary.size());
|
||||
String body =
|
||||
String.format(
|
||||
"Report Filename - Upload status:\n%s",
|
||||
reportSummary
|
||||
.entrySet()
|
||||
.stream()
|
||||
reportSummary.entrySet().stream()
|
||||
.map(
|
||||
(e) ->
|
||||
String.format("%s - %s", e.getKey(), e.getValue() ? "SUCCESS" : "FAILURE"))
|
||||
.collect(Collectors.joining("\n"))));
|
||||
.collect(Collectors.joining("\n")));
|
||||
emailService.sendEmail(EmailMessage.create(subject, body, recipient, sender));
|
||||
}
|
||||
|
||||
private ImmutableList<String> getManifestedFiles(String reportBucketname) {
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
// Copyright 2017 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.reporting.icann;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.util.SendEmailService;
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.Message.RecipientType;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
/** Static utils for emailing reporting results. */
|
||||
public class ReportingEmailUtils {
|
||||
|
||||
@Inject @Config("gSuiteOutgoingEmailAddress") String sender;
|
||||
@Inject @Config("alertRecipientEmailAddress") String recipient;
|
||||
@Inject SendEmailService emailService;
|
||||
@Inject ReportingEmailUtils() {}
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
void emailResults(String subject, String body) {
|
||||
try {
|
||||
Message msg = emailService.createMessage();
|
||||
logger.atInfo().log("Emailing %s", recipient);
|
||||
msg.setFrom(new InternetAddress(sender));
|
||||
msg.addRecipient(RecipientType.TO, new InternetAddress(recipient));
|
||||
msg.setSubject(subject);
|
||||
msg.setText(body);
|
||||
emailService.sendMessage(msg);
|
||||
} catch (Exception e) {
|
||||
logger.atWarning().withCause(e).log("E-mail service failed.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,21 +20,19 @@ import static com.google.common.io.Resources.getResource;
|
|||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.template.soy.SoyFileSet;
|
||||
import com.google.template.soy.parseinfo.SoyTemplateInfo;
|
||||
import com.google.template.soy.tofu.SoyTofu;
|
||||
import com.google.template.soy.tofu.SoyTofu.Renderer;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.SendEmailService;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.Message.RecipientType;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import org.joda.time.LocalDate;
|
||||
|
@ -52,29 +50,26 @@ public class Spec11EmailUtils {
|
|||
.compileToTofu();
|
||||
|
||||
private final SendEmailService emailService;
|
||||
private final String outgoingEmailAddress;
|
||||
private final String alertRecipientAddress;
|
||||
private final String spec11ReplyToAddress;
|
||||
private final InternetAddress outgoingEmailAddress;
|
||||
private final InternetAddress alertRecipientAddress;
|
||||
private final InternetAddress spec11ReplyToAddress;
|
||||
private final ImmutableList<String> spec11WebResources;
|
||||
private final String registryName;
|
||||
private final Retrier retrier;
|
||||
|
||||
@Inject
|
||||
Spec11EmailUtils(
|
||||
SendEmailService emailService,
|
||||
@Config("gSuiteOutgoingEmailAddress") String outgoingEmailAddress,
|
||||
@Config("alertRecipientEmailAddress") String alertRecipientAddress,
|
||||
@Config("spec11ReplyToEmailAddress") String spec11ReplyToAddress,
|
||||
@Config("gSuiteOutgoingEmailAddress") InternetAddress outgoingEmailAddress,
|
||||
@Config("alertRecipientEmailAddress") InternetAddress alertRecipientAddress,
|
||||
@Config("spec11ReplyToEmailAddress") InternetAddress spec11ReplyToAddress,
|
||||
@Config("spec11WebResources") ImmutableList<String> spec11WebResources,
|
||||
@Config("registryName") String registryName,
|
||||
Retrier retrier) {
|
||||
@Config("registryName") String registryName) {
|
||||
this.emailService = emailService;
|
||||
this.outgoingEmailAddress = outgoingEmailAddress;
|
||||
this.alertRecipientAddress = alertRecipientAddress;
|
||||
this.spec11ReplyToAddress = spec11ReplyToAddress;
|
||||
this.spec11WebResources = spec11WebResources;
|
||||
this.registryName = registryName;
|
||||
this.retrier = retrier;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,14 +82,9 @@ public class Spec11EmailUtils {
|
|||
String subject,
|
||||
Set<RegistrarThreatMatches> registrarThreatMatchesSet) {
|
||||
try {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
for (RegistrarThreatMatches registrarThreatMatches : registrarThreatMatchesSet) {
|
||||
emailRegistrar(date, soyTemplateInfo, subject, registrarThreatMatches);
|
||||
}
|
||||
},
|
||||
IOException.class,
|
||||
MessagingException.class);
|
||||
for (RegistrarThreatMatches registrarThreatMatches : registrarThreatMatchesSet) {
|
||||
emailRegistrar(date, soyTemplateInfo, subject, registrarThreatMatches);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Send an alert with the root cause, unwrapping the retrier's RuntimeException
|
||||
sendAlertEmail(
|
||||
|
@ -113,16 +103,15 @@ public class Spec11EmailUtils {
|
|||
String subject,
|
||||
RegistrarThreatMatches registrarThreatMatches)
|
||||
throws MessagingException {
|
||||
Message msg = emailService.createMessage();
|
||||
msg.setSubject(subject);
|
||||
String content = getContent(date, soyTemplateInfo, registrarThreatMatches);
|
||||
msg.setContent(content, "text/html");
|
||||
msg.setHeader("Content-Type", "text/html");
|
||||
msg.setFrom(new InternetAddress(outgoingEmailAddress));
|
||||
msg.addRecipient(
|
||||
RecipientType.TO, new InternetAddress(registrarThreatMatches.registrarEmailAddress()));
|
||||
msg.addRecipient(RecipientType.BCC, new InternetAddress(spec11ReplyToAddress));
|
||||
emailService.sendMessage(msg);
|
||||
emailService.sendEmail(
|
||||
EmailMessage.newBuilder()
|
||||
.setSubject(subject)
|
||||
.setBody(getContent(date, soyTemplateInfo, registrarThreatMatches))
|
||||
.setContentType(MediaType.HTML_UTF_8)
|
||||
.setFrom(outgoingEmailAddress)
|
||||
.addRecipient(new InternetAddress(registrarThreatMatches.registrarEmailAddress()))
|
||||
.setBcc(spec11ReplyToAddress)
|
||||
.build());
|
||||
}
|
||||
|
||||
private String getContent(
|
||||
|
@ -144,7 +133,7 @@ public class Spec11EmailUtils {
|
|||
ImmutableMap.of(
|
||||
"date", date.toString(),
|
||||
"registry", registryName,
|
||||
"replyToEmail", spec11ReplyToAddress,
|
||||
"replyToEmail", spec11ReplyToAddress.getAddress(),
|
||||
"threats", threatMatchMap,
|
||||
"resources", spec11WebResources);
|
||||
renderer.setData(data);
|
||||
|
@ -154,17 +143,13 @@ public class Spec11EmailUtils {
|
|||
/** Sends an e-mail indicating the state of the spec11 pipeline, with a given subject and body. */
|
||||
void sendAlertEmail(String subject, String body) {
|
||||
try {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
Message msg = emailService.createMessage();
|
||||
msg.setFrom(new InternetAddress(outgoingEmailAddress));
|
||||
msg.addRecipient(RecipientType.TO, new InternetAddress(alertRecipientAddress));
|
||||
msg.setSubject(subject);
|
||||
msg.setText(body);
|
||||
emailService.sendMessage(msg);
|
||||
return null;
|
||||
},
|
||||
MessagingException.class);
|
||||
emailService.sendEmail(
|
||||
EmailMessage.newBuilder()
|
||||
.setFrom(outgoingEmailAddress)
|
||||
.addRecipient(alertRecipientAddress)
|
||||
.setBody(body)
|
||||
.setSubject(subject)
|
||||
.build());
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("The spec11 alert e-mail system failed.", e);
|
||||
}
|
||||
|
|
|
@ -15,18 +15,16 @@
|
|||
package google.registry.ui.server;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Iterables.toArray;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.util.EmailMessage;
|
||||
import google.registry.util.SendEmailService;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.internet.AddressException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
|
@ -37,14 +35,14 @@ public class SendEmailUtils {
|
|||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final String gSuiteOutgoingEmailAddress;
|
||||
private final InternetAddress gSuiteOutgoingEmailAddress;
|
||||
private final String gSuiteOutgoingEmailDisplayName;
|
||||
private final SendEmailService emailService;
|
||||
private final ImmutableList<String> registrarChangesNotificationEmailAddresses;
|
||||
|
||||
@Inject
|
||||
public SendEmailUtils(
|
||||
@Config("gSuiteOutgoingEmailAddress") String gSuiteOutgoingEmailAddress,
|
||||
@Config("gSuiteOutgoingEmailAddress") InternetAddress gSuiteOutgoingEmailAddress,
|
||||
@Config("gSuiteOutgoingEmailDisplayName") String gSuiteOutgoingEmailDisplayName,
|
||||
@Config("registrarChangesNotificationEmailAddresses")
|
||||
ImmutableList<String> registrarChangesNotificationEmailAddresses,
|
||||
|
@ -68,10 +66,10 @@ public class SendEmailUtils {
|
|||
public boolean sendEmail(
|
||||
final String subject, String body, ImmutableList<String> additionalAddresses) {
|
||||
try {
|
||||
Message msg = emailService.createMessage();
|
||||
msg.setFrom(
|
||||
new InternetAddress(gSuiteOutgoingEmailAddress, gSuiteOutgoingEmailDisplayName));
|
||||
List<InternetAddress> emails =
|
||||
InternetAddress from =
|
||||
new InternetAddress(
|
||||
gSuiteOutgoingEmailAddress.getAddress(), gSuiteOutgoingEmailDisplayName);
|
||||
ImmutableList<InternetAddress> recipients =
|
||||
Stream.concat(
|
||||
registrarChangesNotificationEmailAddresses.stream(), additionalAddresses.stream())
|
||||
.map(
|
||||
|
@ -88,20 +86,23 @@ public class SendEmailUtils {
|
|||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(toImmutableList());
|
||||
if (emails.isEmpty()) {
|
||||
if (recipients.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
msg.addRecipients(Message.RecipientType.TO, toArray(emails, InternetAddress.class));
|
||||
msg.setSubject(subject);
|
||||
msg.setText(body);
|
||||
emailService.sendMessage(msg);
|
||||
emailService.sendEmail(
|
||||
EmailMessage.newBuilder()
|
||||
.setBody(body)
|
||||
.setSubject(subject)
|
||||
.setRecipients(recipients)
|
||||
.setFrom(from)
|
||||
.build());
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().withCause(t).log(
|
||||
"Could not email to addresses %s with subject '%s'.",
|
||||
Joiner.on(", ").join(registrarChangesNotificationEmailAddresses), subject);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Sends an email from Nomulus to the registrarChangesNotificationEmailAddresses.
|
||||
|
|
|
@ -11,6 +11,7 @@ java_library(
|
|||
"//third_party/jaxb",
|
||||
"//third_party/objectify:objectify-v4_1",
|
||||
"@com_google_appengine_api_1_0_sdk",
|
||||
"@com_google_auto_value",
|
||||
"@com_google_code_findbugs_jsr305",
|
||||
"@com_google_dagger",
|
||||
"@com_google_errorprone_error_prone_annotations",
|
||||
|
|
94
java/google/registry/util/EmailMessage.java
Normal file
94
java/google/registry/util/EmailMessage.java
Normal file
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2019 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.util;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.net.MediaType;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
/** Value class representing the content and metadata of an email. */
|
||||
@AutoValue
|
||||
public abstract class EmailMessage {
|
||||
|
||||
public abstract String subject();
|
||||
public abstract String body();
|
||||
public abstract ImmutableList<InternetAddress> recipients();
|
||||
public abstract InternetAddress from();
|
||||
public abstract Optional<InternetAddress> bcc();
|
||||
public abstract Optional<MediaType> contentType();
|
||||
public abstract Optional<Attachment> attachment();
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new AutoValue_EmailMessage.Builder();
|
||||
}
|
||||
|
||||
public static EmailMessage create(
|
||||
String subject, String body, InternetAddress recipient, InternetAddress from) {
|
||||
return newBuilder()
|
||||
.setSubject(subject)
|
||||
.setBody(body)
|
||||
.setRecipients(ImmutableList.of(recipient))
|
||||
.setFrom(from)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Builder for {@link EmailMessage}. */
|
||||
@AutoValue.Builder
|
||||
public abstract static class Builder {
|
||||
|
||||
public abstract Builder setSubject(String subject);
|
||||
public abstract Builder setBody(String body);
|
||||
public abstract Builder setRecipients(Collection<InternetAddress> recipients);
|
||||
public abstract Builder setFrom(InternetAddress from);
|
||||
public abstract Builder setBcc(InternetAddress bcc);
|
||||
public abstract Builder setContentType(MediaType contentType);
|
||||
public abstract Builder setAttachment(Attachment attachment);
|
||||
|
||||
abstract ImmutableList.Builder<InternetAddress> recipientsBuilder();
|
||||
|
||||
public Builder addRecipient(InternetAddress value) {
|
||||
recipientsBuilder().add(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract EmailMessage build();
|
||||
}
|
||||
|
||||
/** An attachment to the email, if one exists. */
|
||||
@AutoValue
|
||||
public abstract static class Attachment {
|
||||
|
||||
public abstract MediaType contentType();
|
||||
public abstract String filename();
|
||||
public abstract String content();
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new AutoValue_EmailMessage_Attachment.Builder();
|
||||
}
|
||||
|
||||
/** Builder for {@link Attachment}. */
|
||||
@AutoValue.Builder
|
||||
public abstract static class Builder {
|
||||
|
||||
public abstract Builder setContentType(MediaType contentType);
|
||||
public abstract Builder setFilename(String filename);
|
||||
public abstract Builder setContent(String content);
|
||||
public abstract Attachment build();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,29 +14,78 @@
|
|||
|
||||
package google.registry.util;
|
||||
|
||||
import static com.google.common.collect.Iterables.toArray;
|
||||
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.util.EmailMessage.Attachment;
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.mail.BodyPart;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.Message.RecipientType;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.Multipart;
|
||||
import javax.mail.Session;
|
||||
import javax.mail.Transport;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeBodyPart;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.mail.internet.MimeMultipart;
|
||||
|
||||
/** Wrapper around javax.mail's Transport.send that can be mocked for testing purposes. */
|
||||
/**
|
||||
* Wrapper around javax.mail's email creation and sending functionality. Encompasses a retry policy
|
||||
* as well.
|
||||
*/
|
||||
@Singleton
|
||||
public class SendEmailService {
|
||||
|
||||
@Inject
|
||||
SendEmailService() {};
|
||||
private final Retrier retrier;
|
||||
private final TransportEmailSender transportEmailSender;
|
||||
|
||||
/** Returns a new MimeMessage using default App Engine transport settings. */
|
||||
public Message createMessage() {
|
||||
return new MimeMessage(Session.getDefaultInstance(new Properties(), null));
|
||||
@Inject
|
||||
SendEmailService(Retrier retrier, TransportEmailSender transportEmailSender) {
|
||||
this.retrier = retrier;
|
||||
this.transportEmailSender = transportEmailSender;
|
||||
}
|
||||
|
||||
/** Sends a message using default App Engine transport. */
|
||||
public void sendMessage(Message msg) throws MessagingException {
|
||||
Transport.send(msg);
|
||||
/**
|
||||
* Converts the provided message content into a {@link javax.mail.Message} and sends it with
|
||||
* retry on transient failures.
|
||||
*/
|
||||
public void sendEmail(EmailMessage emailMessage) {
|
||||
retrier.callWithRetry(
|
||||
() -> {
|
||||
Message 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);
|
||||
}
|
||||
if (emailMessage.bcc().isPresent()) {
|
||||
msg.addRecipient(RecipientType.BCC, emailMessage.bcc().get());
|
||||
}
|
||||
msg.setContent(multipart);
|
||||
msg.saveChanges();
|
||||
transportEmailSender.sendMessage(msg);
|
||||
},
|
||||
IOException.class,
|
||||
MessagingException.class);
|
||||
}
|
||||
}
|
||||
|
|
34
java/google/registry/util/TransportEmailSender.java
Normal file
34
java/google/registry/util/TransportEmailSender.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2019 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.util;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.Transport;
|
||||
|
||||
/** Wrapper for sending email so that we can test {@link google.registry.util.SendEmailService}. */
|
||||
@Singleton
|
||||
class TransportEmailSender {
|
||||
|
||||
@Inject
|
||||
TransportEmailSender() {}
|
||||
|
||||
/** Sends a message using default App Engine transport. */
|
||||
void sendMessage(Message msg) throws MessagingException {
|
||||
Transport.send(msg);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue