blob: 4dda7f000461d540d8abc887a950e71e646c81bd [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// 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 com.google.gerrit.server.mail.send;
import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS;
import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
import static java.util.Objects.requireNonNull;
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
import com.google.common.base.Throwables;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.EmailHeader;
import com.google.gerrit.entities.EmailHeader.AddressList;
import com.google.gerrit.entities.EmailHeader.StringEmailHeader;
import com.google.gerrit.exceptions.EmailException;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
import com.google.gerrit.mail.MailHeader;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.RetryableAction.ActionType;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.jbcsrc.api.SoySauce;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import org.apache.james.mime4j.dom.field.FieldName;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.util.SystemReader;
/** Represents an email notification for some event that can be sent to interested parties. */
@AutoFactory
public final class OutgoingEmail {
/** Provides content, recipients and any customizations of the email. */
public interface EmailDecorator {
/**
* Stores the reference to the email for the subsequent calls.
*
* <p>Both init and populateEmailContent can be called multiply times in case of retries. Init
* is therefore responsible for clearing up any changes which are not idempotent and
* initializing data for use in populateEmailContent.
*
* <p>Can be used to adjust any of the behaviour of the {@link
* OutgoingEmail#populateEmailContent}.
*/
void init(OutgoingEmail email) throws EmailException;
/**
* Populate headers, recipients and body of the email.
*
* <p>Method operates on the email provided in the init method.
*
* <p>By default, all the contents and parameters of the email should be set in this method.
*/
void populateEmailContent() throws EmailException;
/** If returns false email is not sent to any recipients. */
default boolean shouldSendMessage() {
return true;
}
/**
* Evaluates whether account can be added to the list of recipients.
*
* @param rcpt the recipient for which it should be checker whether it can be added to the list
* of recipients
* @throws PermissionBackendException thrown if checking permissions fails
*/
default boolean isRecipientAllowed(Account.Id rcpt) throws PermissionBackendException {
return true;
}
/**
* Evaluates whether email can be added to the list of recipients.
*
* @param rcpt the recipient for which it should be checker whether it can be added to the list
* of recipients
* @throws PermissionBackendException thrown if checking permissions fails
*/
default boolean isRecipientAllowed(Address rcpt) throws PermissionBackendException {
return true;
}
}
private static final String SOY_TEMPLATE_NAMESPACE = "com.google.gerrit.server.mail.template";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private String messageClass;
private final Set<Account.Id> rcptTo = new HashSet<>();
private final Map<String, EmailHeader> headers = new LinkedHashMap<>();
private final Set<Address> smtpRcptTo = new HashSet<>();
private final Set<Address> smtpBccRcptTo = new HashSet<>();
private Address smtpFromAddress;
private StringBuilder textBody;
private ArrayList<SanitizedContent> htmlBodySections;
private MessageIdGenerator.MessageId messageId;
private Map<String, Object> soyContext;
private Map<String, Object> soyContextEmailData;
private List<String> footers;
private final EmailArguments args;
private Account.Id fromId;
private NotifyResolver.Result notify = NotifyResolver.Result.all();
private final EmailDecorator templateProvider;
private ArrayList<EmailResource> htmlResources;
public OutgoingEmail(
@Provided EmailArguments args, String messageClass, EmailDecorator templateProvider) {
this.args = args;
this.messageClass = messageClass;
this.templateProvider = templateProvider;
}
/** Specify the account that triggered the notification. */
public void setFrom(Account.Id id) {
fromId = id;
}
/** Get the account that triggered the notification. */
public Account.Id getFrom() {
return fromId;
}
/** Set how widely the email notification is allowed to be sent. */
public void setNotify(NotifyResolver.Result notify) {
this.notify = requireNonNull(notify);
}
/** Returns the setting that controls how widely the email notification is allowed to be sent. */
public NotifyResolver.Result getNotify() {
return this.notify;
}
/** Set identifier for the email. Every email must have one. */
public void setMessageId(MessageIdGenerator.MessageId messageId) {
this.messageId = messageId;
}
private String constructTextEmail() {
soyContext.put("body", textBody.toString());
soyContext.put("footer", textTemplate("Footer"));
return textTemplate("Email");
}
private String constructHtmlEmail() {
soyContext.put("body_sections_html", htmlBodySections);
soyContext.put("footer_html", soyHtmlTemplate("FooterHtml"));
return soyHtmlTemplate("EmailHtml").toString();
}
/** Format and enqueue the message for delivery. */
public void send() throws EmailException {
try {
args.retryHelper
.action(
ActionType.SEND_EMAIL,
"sendEmail",
() -> {
sendImpl();
return null;
})
.retryWithTrace(Exception.class::isInstance)
.call();
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
Throwables.throwIfInstanceOf(e, EmailException.class);
throw new EmailException("sending email failed", e);
}
}
private void sendImpl() throws EmailException {
if (!args.emailSender.isEnabled()) {
// Server has explicitly disabled email sending.
//
logger.atFine().log(
"Not sending '%s': Email sending is disabled by server config", messageClass);
return;
}
init();
if (!notify.shouldNotify()) {
logger.atFine().log("Not sending '%s': Notify handling is NONE", messageClass);
return;
}
populateEmailContent();
if (messageId == null) {
throw new IllegalStateException("All emails must have a messageId");
}
Set<Address> smtpRcptToPlaintextOnly = new HashSet<>();
if (shouldSendMessage()) {
if (fromId != null) {
Optional<AccountState> fromUser = args.accountCache.get(fromId);
if (fromUser.isPresent()) {
GeneralPreferencesInfo senderPrefs = fromUser.get().generalPreferences();
CurrentUser user = args.currentUserProvider.get();
boolean isImpersonating = user.isIdentifiedUser() && user.isImpersonating();
if (isImpersonating && user.getAccountId() != fromId) {
// This should not be possible, if this is the case it means the RequestContext is not
// set up correctly.
throw new EmailException(
String.format(
"User %s is sending email from %s, while acting on behalf of %s",
user.asIdentifiedUser().getRealUser().getAccountId(),
fromId,
user.getAccountId()));
}
if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
// Include the sender in email if they enabled email notifications on their own
// comments.
//
logger.atFine().log(
"CC email sender %s because the email strategy of this user is %s",
fromUser.get().account().id(), CC_ON_OWN_COMMENTS);
addByAccountId(RecipientType.CC, fromId);
} else if (isImpersonating) {
// If we are impersonating a user, make sure they receive a CC of
// this message regardless of email strategy, unless email notifications are explicitly
// disabled for this user. This way they can always review and audit what we sent
// on their behalf to others.
logger.atFine().log(
"CC email sender %s because the email is sent on behalf of and email notifications"
+ " are enabled for this user.",
fromUser.get().account().id());
addByAccountId(RecipientType.CC, fromId);
} else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) {
// If they don't want a copy, but we queued one up anyway,
// drop them from the recipient lists, but only if the user is not being impersonated.
//
logger.atFine().log(
"Not CCing email sender %s because the email strategy of this user is not %s but"
+ " %s",
fromUser.get().account().id(),
CC_ON_OWN_COMMENTS,
senderPrefs != null ? senderPrefs.getEmailStrategy() : null);
removeUser(fromUser.get().account());
}
}
}
// Check the preferences of all recipients. If any user has disabled
// his email notifications then drop him from recipients' list.
// In addition, check if users only want to receive plaintext email.
for (Account.Id id : rcptTo) {
Optional<AccountState> thisUser = args.accountCache.get(id);
if (thisUser.isPresent()) {
Account thisUserAccount = thisUser.get().account();
GeneralPreferencesInfo prefs = thisUser.get().generalPreferences();
if (prefs == null || prefs.getEmailStrategy() == DISABLED) {
logger.atFine().log(
"Not emailing account %s because user has set email strategy to %s", id, DISABLED);
removeUser(thisUserAccount);
} else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) {
logger.atFine().log(
"Removing account %s from HTML email because user prefers plain text emails", id);
removeUser(thisUserAccount);
smtpRcptToPlaintextOnly.add(
Address.create(thisUserAccount.fullName(), thisUserAccount.preferredEmail()));
}
}
if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) {
logger.atFine().log("Not sending '%s': No SMTP recipients", messageClass);
return;
}
}
// Set Reply-To only if it hasn't been set by a child class
// Reply-To will already be populated for the message types where Gerrit supports
// inbound email replies.
if (!headers.containsKey(FieldName.REPLY_TO)) {
StringJoiner j = new StringJoiner(", ");
if (fromId != null) {
Address address = toAddress(fromId);
if (address != null) {
j.add(address.email());
}
}
// For users who prefer plaintext, this comes at the cost of not being
// listed in the multipart To and Cc headers. We work around this by adding
// all users to the Reply-To address in both the plaintext and multipart
// email. We should exclude any BCC addresses from reply-to, because they should be
// invisible to other recipients.
Sets.difference(Sets.union(smtpRcptTo, smtpRcptToPlaintextOnly), smtpBccRcptTo).stream()
.forEach(a -> j.add(a.email()));
setHeader(FieldName.REPLY_TO, j.toString());
}
OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args();
va.messageClass = messageClass;
va.smtpFromAddress = smtpFromAddress;
va.smtpRcptTo = smtpRcptTo;
va.headers = headers;
va.body = constructTextEmail();
if (useHtml()) {
va.htmlBody = constructHtmlEmail();
} else {
va.htmlBody = null;
}
Set<Address> intersection = Sets.intersection(va.smtpRcptTo, smtpRcptToPlaintextOnly);
if (!intersection.isEmpty()) {
logger.atSevere().log("Email '%s' will be sent twice to %s", messageClass, intersection);
}
if (!va.smtpRcptTo.isEmpty()) {
// Send multipart message
addMessageId(va, "-HTML");
if (!validateEmail(va)) return;
logger.atFine().log(
"Sending multipart '%s' from %s to %s",
messageClass, va.smtpFromAddress, va.smtpRcptTo);
args.emailSender.send(
va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody, htmlResources);
}
if (!smtpRcptToPlaintextOnly.isEmpty()) {
addMessageId(va, "-PLAIN");
// Send plaintext message
Map<String, EmailHeader> shallowCopy = new HashMap<>();
shallowCopy.putAll(headers);
// Remove To and Cc
shallowCopy.remove(FieldName.TO);
shallowCopy.remove(FieldName.CC);
for (Address a : smtpRcptToPlaintextOnly) {
// Add new To
AddressList to = new AddressList();
to.add(a);
shallowCopy.put(FieldName.TO, to);
}
if (!validateEmail(va)) return;
logger.atFine().log(
"Sending plaintext '%s' from %s to %s",
messageClass, va.smtpFromAddress, smtpRcptToPlaintextOnly);
args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body);
}
}
}
private boolean validateEmail(OutgoingEmailValidationListener.Args va) {
for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
try {
validator.validateOutgoingEmail(va);
} catch (ValidationException e) {
logger.atFine().log(
"Not sending '%s': Rejected by outgoing email validator: %s",
messageClass, e.getMessage());
return false;
}
}
return true;
}
// All message ids must start with < and end with >. Also, they must have @domain and no spaces.
private void addMessageId(OutgoingEmailValidationListener.Args va, String suffix) {
if (messageId != null) {
String message = "<" + messageId.id() + suffix + "@" + getGerritHost() + ">";
message = message.replaceAll("\\s", "");
va.headers.put(FieldName.MESSAGE_ID, new StringEmailHeader(message));
}
}
/**
* Setup the message headers and envelope (TO, CC, BCC).
*
* @throws EmailException if an error occurred.
*/
public void init() throws EmailException {
soyContext = new HashMap<>();
footers = new ArrayList<>();
soyContextEmailData = new HashMap<>();
htmlResources = new ArrayList<>();
smtpFromAddress = args.fromAddressGenerator.get().from(fromId);
setHeader(FieldName.DATE, Instant.now());
headers.put(FieldName.FROM, new AddressList(smtpFromAddress));
headers.put(FieldName.TO, new AddressList());
headers.put(FieldName.CC, new AddressList());
setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
setHeader(MailHeader.MESSAGE_TYPE.fieldName(), messageClass);
addFooter(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
textBody = new StringBuilder();
htmlBodySections = new ArrayList<>();
if (fromId != null && args.fromAddressGenerator.get().isGenericAddress(fromId)) {
appendText(getFromLine());
}
templateProvider.init(this);
}
private String getFromLine() {
StringBuilder f = new StringBuilder();
Optional<Account> account = args.accountCache.get(fromId).map(AccountState::account);
if (account.isPresent()) {
String name = account.get().fullName();
String email = account.get().preferredEmail();
if ((name != null && !name.isEmpty()) || (email != null && !email.isEmpty())) {
f.append("From");
if (name != null && !name.isEmpty()) {
f.append(" ").append(name);
}
if (email != null && !email.isEmpty()) {
f.append(" <").append(email).append(">");
}
f.append(":\n\n");
}
}
return f.toString();
}
public String getGerritHost() {
Optional<String> gerritUrl = args.urlFormatter.get().getWebUrl();
if (gerritUrl.isPresent()) {
try {
return new URL(gerritUrl.get()).getHost();
} catch (MalformedURLException e) {
// Try something else.
}
}
// Fall back onto whatever the local operating system thinks
// this server is called. We hopefully didn't get here as a
// good admin would have configured the canonical url.
//
return SystemReader.getInstance().getHostname();
}
@Nullable
public String getSettingsUrl() {
return args.urlFormatter.get().getSettingsUrl().map(EmailArguments::addUspParam).orElse(null);
}
@Nullable
public String getSettingsUrl(String section) {
return args.urlFormatter
.get()
.getSettingsUrl(section)
.map(EmailArguments::addUspParam)
.orElse(null);
}
/** Set a header in the outgoing message. */
public void setHeader(String name, String value) {
headers.put(name, new StringEmailHeader(value));
}
/** Remove a header from the outgoing message. */
public void removeHeader(String name) {
headers.remove(name);
}
/** Set a date header in the outgoing message. */
public void setHeader(String name, Instant date) {
headers.put(name, new EmailHeader.Date(date));
}
/** Append text to the outgoing email body. */
public void appendText(String text) {
if (text != null) {
textBody.append(text);
}
}
/** Append html to the outgoing email body. */
public void appendHtml(SanitizedContent html) {
if (html != null) {
htmlBodySections.add(html);
}
}
/** Lookup a human readable name for an account, usually the "full name". */
public String getNameFor(@Nullable Account.Id accountId) {
if (accountId == null) {
return args.gerritPersonIdent.get().getName();
}
Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account);
String name = null;
if (account.isPresent()) {
name = account.get().fullName();
if (name == null) {
name = account.get().preferredEmail();
}
}
if (name == null) {
name = args.anonymousCowardName + " #" + accountId;
}
return name;
}
/**
* Gets the human readable name and email for an account; if neither are available, returns the
* Anonymous Coward name.
*
* @param accountId user to fetch.
* @return name/email of account, or Anonymous Coward if unset.
*/
public String getNameEmailFor(@Nullable Account.Id accountId) {
if (accountId == null) {
PersonIdent gerritIdent = args.gerritPersonIdent.get();
return gerritIdent.getName() + " <" + gerritIdent.getEmailAddress() + ">";
}
Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account);
if (account.isPresent()) {
String name = account.get().fullName();
String email = account.get().preferredEmail();
if (name != null && email != null) {
return name + " <" + email + ">";
} else if (name != null) {
return name;
} else if (email != null) {
return email;
}
}
return args.anonymousCowardName + " #" + accountId;
}
/**
* Gets the human readable name and email for an account; if both are unavailable, returns the
* username. If no username is set, this function returns null.
*
* @param accountId user to fetch.
* @return name/email of account, username, or null if unset or the accountId is null.
*/
@Nullable
public String getUserNameEmailFor(@Nullable Account.Id accountId) {
if (accountId == null) {
return null;
}
Optional<AccountState> accountState = args.accountCache.get(accountId);
if (!accountState.isPresent()) {
return null;
}
Account account = accountState.get().account();
String name = account.fullName();
String email = account.preferredEmail();
if (name != null && email != null) {
return name + " <" + email + ">";
} else if (email != null) {
return email;
} else if (name != null) {
return name;
}
return accountState.get().userName().orElse(null);
}
private boolean shouldSendMessage() {
if (textBody.length() == 0) {
// If we have no message body, don't send.
logger.atFine().log("Not sending '%s': No message body", messageClass);
return false;
}
if (smtpRcptTo.isEmpty()) {
// If we have nobody to send this message to, then all of our
// selection filters previously for this type of message were
// unable to match a destination. Don't bother sending it.
logger.atFine().log("Not sending '%s': No recipients", messageClass);
return false;
}
if (notify.accounts().isEmpty()
&& smtpRcptTo.size() == 1
&& rcptTo.size() == 1
&& rcptTo.contains(fromId)) {
// If the only recipient is also the sender, don't bother.
//
logger.atFine().log("Not sending '%s': Sender is only recipient", messageClass);
return false;
}
return templateProvider.shouldSendMessage();
}
/**
* Adds a recipient that the email will be sent to.
*
* @param rt category of recipient (TO, CC, BCC)
* @param addr Name and email of the recipient.
*/
public final void addByEmail(RecipientType rt, Address addr) {
addByEmail(rt, addr, false);
}
/**
* Adds a recipient that the email will be sent to.
*
* @param rt category of recipient (TO, CC, BCC).
* @param addr Name and email of the recipient.
* @param override if the recipient was added previously and override is false no change is made
* regardless of {@code rt}.
*/
public final void addByEmail(RecipientType rt, Address addr, boolean override) {
try {
if (isRecipientAllowed(addr)) {
add(rt, addr, override);
}
} catch (PermissionBackendException e) {
logger.atSevere().withCause(e).log("Error checking permissions for email address: %s", addr);
}
}
/**
* Returns whether this email is allowed to be sent to the given address
*
* @param addr email address of recipient.
* @throws PermissionBackendException thrown if checking a permission fails due to an error in the
* permission backend
*/
public boolean isRecipientAllowed(Address addr) throws PermissionBackendException {
return templateProvider.isRecipientAllowed(addr);
}
/**
* Adds a recipient that the email will be sent to.
*
* @param rt category of recipient (TO, CC, BCC)
* @param to Gerrit Account of the recipient.
*/
public void addByAccountId(RecipientType rt, Account.Id to) {
addByAccountId(rt, to, false);
}
/**
* Adds a recipient that the email will be sent to.
*
* @param rt category of recipient (TO, CC, BCC)
* @param to Gerrit Account of the recipient.
* @param override if the recipient was added previously and override is false no change is made
* regardless of {@code rt}.
*/
public void addByAccountId(RecipientType rt, Account.Id to, boolean override) {
try {
if (rcptTo.contains(to) || !isRecipientAllowed(to)) {
return;
}
Address addr = toAddress(to);
if (addr == null) {
logger.atFine().log("Not emailing account %s because user has no preferred email", to);
return;
}
rcptTo.add(to);
add(rt, addr, override);
} catch (PermissionBackendException e) {
logger.atSevere().withCause(e).log("Error checking permissions for account: %s", to);
}
}
/**
* Returns whether this email is allowed to be sent to the given account
*
* @param to account.
* @throws PermissionBackendException thrown if checking a permission fails due to an error in the
* permission backend
*/
public boolean isRecipientAllowed(Account.Id to) throws PermissionBackendException {
return templateProvider.isRecipientAllowed(to);
}
private final void add(RecipientType rt, Address addr, boolean override) {
if (addr != null && addr.email() != null && addr.email().length() > 0) {
if (!args.validator.isValid(addr.email())) {
logger.atWarning().log("Not emailing %s (invalid email address)", addr.email());
} else if (args.emailSender.canEmail(addr.email())) {
if (!smtpRcptTo.add(addr)) {
if (!override) {
return;
}
((AddressList) headers.get(FieldName.TO)).remove(addr.email());
((AddressList) headers.get(FieldName.CC)).remove(addr.email());
smtpBccRcptTo.remove(addr);
}
switch (rt) {
case TO:
((AddressList) headers.get(FieldName.TO)).add(addr);
break;
case CC:
((AddressList) headers.get(FieldName.CC)).add(addr);
break;
case BCC:
smtpBccRcptTo.add(addr);
break;
}
}
}
}
/** Returns preferred email address for the account. */
@Nullable
public Address toAddress(Account.Id id) {
Optional<Account> accountState = args.accountCache.get(id).map(AccountState::account);
if (!accountState.isPresent()) {
return null;
}
Account account = accountState.get();
String e = account.preferredEmail();
if (!account.isActive() || e == null) {
return null;
}
return Address.create(account.fullName(), e);
}
/** Returns the type of notification being sent. */
public String getMessageClass() {
return messageClass;
}
/** Set recipients, headers, body of the email. */
public void populateEmailContent() throws EmailException {
for (RecipientType recipientType : notify.accounts().keySet()) {
notify.accounts().get(recipientType).stream().forEach(a -> addByAccountId(recipientType, a));
}
addSoyParam("messageClass", messageClass);
addSoyParam("footers", footers);
addSoyEmailDataParam("settingsUrl", getSettingsUrl());
addSoyEmailDataParam("instanceName", getInstanceName());
addSoyEmailDataParam("gerritHost", getGerritHost());
addSoyParam("email", soyContextEmailData);
templateProvider.populateEmailContent();
}
/** Adds param to the data map passed into soy when rendering templates. */
public void addSoyParam(String key, Object value) {
soyContext.put(key, value);
}
/** Adds entry to the `email` param passed to the soy when rendering templates. */
public void addSoyEmailDataParam(String key, Object value) {
soyContextEmailData.put(key, value);
}
/**
* Add a line to email footer with additional information. Typically, in the form of {@literal
* <key>: <value>}.
*/
public void addFooter(String footer) {
footers.add(footer);
}
/**
* Add a resource that can be referenced in HTML code using their {@link EmailResource#contentId}.
*/
public void addHtmlResource(EmailResource resource) {
htmlResources.add(resource);
}
private String getInstanceName() {
return args.instanceNameProvider.get();
}
/** Renders a soy template of kind="text". */
public String textTemplate(String name) {
return configureRenderer(name).renderText().get();
}
/** Renders a soy template of kind="html". */
public SanitizedContent soyHtmlTemplate(String name) {
return configureRenderer(name).renderHtml().get();
}
/** Renders a soy template of kind="css". */
@UsedAt(UsedAt.Project.GOOGLE)
public SanitizedContent soyCssTemplate(String name) {
return configureRenderer(name).renderCss().get();
}
/** Configures a soy renderer for the given template name and rendering data map. */
private SoySauce.Renderer configureRenderer(String templateName) {
int baseNameIndex = templateName.indexOf("_");
// In case there are multiple templates in file (now only InboundEmailRejection and
// InboundEmailRejectionHtml).
String fileNamespace =
baseNameIndex == -1 ? templateName : templateName.substring(0, baseNameIndex);
String templateInFileNamespace =
String.join(".", SOY_TEMPLATE_NAMESPACE, fileNamespace, templateName);
String templateInCommonNamespace = String.join(".", SOY_TEMPLATE_NAMESPACE, templateName);
SoySauce soySauce = args.soySauce.get();
// For backwards compatibility with existing customizations and plugin templates with the
// old non-unique namespace.
String fullTemplateName =
soySauce.hasTemplate(templateInFileNamespace)
? templateInFileNamespace
: templateInCommonNamespace;
return soySauce.renderTemplate(fullTemplateName).setData(soyContext);
}
/** Remove user from the multipart email recipients. */
private void removeUser(Account user) {
String fromEmail = user.preferredEmail();
for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext(); ) {
if (j.next().email().equals(fromEmail)) {
j.remove();
}
}
for (Map.Entry<String, EmailHeader> entry : headers.entrySet()) {
// Don't remove fromEmail from the "From" header though!
if (entry.getValue() instanceof AddressList && !entry.getKey().equals("From")) {
((AddressList) entry.getValue()).remove(fromEmail);
}
}
}
/** Return true, if the email should include html body. */
public boolean useHtml() {
return args.settings.html;
}
}