com.google.gerrit.server.mail.send.OutgoingEmail Maven / Gradle / Ivy
// 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.mail.EmailFactories;
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.URI;
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.
     *
     * 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.
     *
     * 
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.
     *
     * 
Method operates on the email provided in the init method.
     *
     * 
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 rcptTo = new HashSet<>();
  private final Map headers = new LinkedHashMap<>();
  private final Set smtpRcptTo = new HashSet<>();
  private final Set smtpBccRcptTo = new HashSet<>();
  private Address smtpFromAddress;
  private StringBuilder textBody;
  private ArrayList htmlBodySections;
  private MessageIdGenerator.MessageId messageId;
  private Map soyContext;
  private Map soyContextEmailData;
  private List footers;
  private final EmailArguments args;
  private Account.Id fromId;
  private NotifyResolver.Result notify = NotifyResolver.Result.all();
  private final EmailDecorator templateProvider;
  private ArrayList 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 {
      @SuppressWarnings("unused")
      var unused =
          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 smtpRcptToPlaintextOnly = new HashSet<>();
    if (shouldSendMessage()) {
      if (fromId != null) {
        Optional 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 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 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 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 = 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 gerritUrl = args.urlFormatter.get().getWebUrl();
    if (gerritUrl.isPresent()) {
      try {
        return URI.create(gerritUrl.get()).toURL().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 = 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 = 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 = 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);
          case CC -> ((AddressList) headers.get(FieldName.CC)).add(addr);
          case BCC -> smtpBccRcptTo.add(addr);
        }
      }
    }
  }
  /** Returns preferred email address for the account. */
  @Nullable
  public Address toAddress(Account.Id id) {
    Optional 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("messageClassDisplay", EmailFactories.messageClassDisplay(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
   * : }.
   */
  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.instanceName;
  }
  /** 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 j = smtpRcptTo.iterator(); j.hasNext(); ) {
      if (j.next().email().equals(fromEmail)) {
        j.remove();
      }
    }
    for (Map.Entry 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;
  }
}