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:
gbrodman 2019-03-11 14:30:41 -07:00 committed by Ben McIlwain
parent 9823ee7fcf
commit 50e0a9b532
35 changed files with 773 additions and 614 deletions

View file

@ -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);
}

View file

@ -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(

View file

@ -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) {

View file

@ -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.");
}
}
}

View file

@ -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);
}