LCOV - code coverage report
Current view: top level - server/mail/send - OutgoingEmail.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 272 318 85.5 %
Date: 2022-11-19 15:00:39 Functions: 39 41 95.1 %

          Line data    Source code
       1             : // Copyright (C) 2016 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.mail.send;
      16             : 
      17             : import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS;
      18             : import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
      19             : import static java.util.Objects.requireNonNull;
      20             : 
      21             : import com.google.common.base.Throwables;
      22             : import com.google.common.collect.Sets;
      23             : import com.google.common.flogger.FluentLogger;
      24             : import com.google.gerrit.common.Nullable;
      25             : import com.google.gerrit.entities.Account;
      26             : import com.google.gerrit.entities.Address;
      27             : import com.google.gerrit.entities.EmailHeader;
      28             : import com.google.gerrit.entities.EmailHeader.AddressList;
      29             : import com.google.gerrit.entities.EmailHeader.StringEmailHeader;
      30             : import com.google.gerrit.exceptions.EmailException;
      31             : import com.google.gerrit.extensions.api.changes.RecipientType;
      32             : import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
      33             : import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
      34             : import com.google.gerrit.mail.MailHeader;
      35             : import com.google.gerrit.server.CurrentUser;
      36             : import com.google.gerrit.server.account.AccountState;
      37             : import com.google.gerrit.server.change.NotifyResolver;
      38             : import com.google.gerrit.server.permissions.PermissionBackendException;
      39             : import com.google.gerrit.server.update.RetryableAction.ActionType;
      40             : import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
      41             : import com.google.gerrit.server.validators.ValidationException;
      42             : import com.google.template.soy.jbcsrc.api.SoySauce;
      43             : import java.net.MalformedURLException;
      44             : import java.net.URL;
      45             : import java.time.Instant;
      46             : import java.util.ArrayList;
      47             : import java.util.Collection;
      48             : import java.util.HashMap;
      49             : import java.util.HashSet;
      50             : import java.util.Iterator;
      51             : import java.util.LinkedHashMap;
      52             : import java.util.List;
      53             : import java.util.Map;
      54             : import java.util.Optional;
      55             : import java.util.Set;
      56             : import java.util.StringJoiner;
      57             : import org.apache.james.mime4j.dom.field.FieldName;
      58             : import org.eclipse.jgit.lib.PersonIdent;
      59             : import org.eclipse.jgit.util.SystemReader;
      60             : 
      61             : /** Sends an email to one or more interested parties. */
      62             : public abstract class OutgoingEmail {
      63             :   private static final String SOY_TEMPLATE_NAMESPACE = "com.google.gerrit.server.mail.template";
      64         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      65             : 
      66             :   protected String messageClass;
      67         106 :   private final Set<Account.Id> rcptTo = new HashSet<>();
      68             :   private final Map<String, EmailHeader> headers;
      69         106 :   private final Set<Address> smtpRcptTo = new HashSet<>();
      70         106 :   private final Set<Address> smtpBccRcptTo = new HashSet<>();
      71             :   private Address smtpFromAddress;
      72             :   private StringBuilder textBody;
      73             :   private StringBuilder htmlBody;
      74             :   private MessageIdGenerator.MessageId messageId;
      75             :   protected Map<String, Object> soyContext;
      76             :   protected Map<String, Object> soyContextEmailData;
      77             :   protected List<String> footers;
      78             :   protected final EmailArguments args;
      79             :   protected Account.Id fromId;
      80         106 :   protected NotifyResolver.Result notify = NotifyResolver.Result.all();
      81             : 
      82         106 :   protected OutgoingEmail(EmailArguments args, String messageClass) {
      83         106 :     this.args = args;
      84         106 :     this.messageClass = messageClass;
      85         106 :     this.headers = new LinkedHashMap<>();
      86         106 :   }
      87             : 
      88             :   public void setFrom(Account.Id id) {
      89         103 :     fromId = id;
      90         103 :   }
      91             : 
      92             :   public void setNotify(NotifyResolver.Result notify) {
      93         103 :     this.notify = requireNonNull(notify);
      94         103 :   }
      95             : 
      96             :   public void setMessageId(MessageIdGenerator.MessageId messageId) {
      97         106 :     this.messageId = messageId;
      98         106 :   }
      99             : 
     100             :   /** Format and enqueue the message for delivery. */
     101             :   public void send() throws EmailException {
     102             :     try {
     103         106 :       args.retryHelper
     104         106 :           .action(
     105             :               ActionType.SEND_EMAIL,
     106             :               "sendEmail",
     107             :               () -> {
     108         106 :                 sendImpl();
     109         106 :                 return null;
     110             :               })
     111         106 :           .retryWithTrace(Exception.class::isInstance)
     112         106 :           .call();
     113           0 :     } catch (Exception e) {
     114           0 :       Throwables.throwIfUnchecked(e);
     115           0 :       Throwables.throwIfInstanceOf(e, EmailException.class);
     116           0 :       throw new EmailException("sending email failed", e);
     117         106 :     }
     118         106 :   }
     119             : 
     120             :   private void sendImpl() throws EmailException {
     121         106 :     if (!args.emailSender.isEnabled()) {
     122             :       // Server has explicitly disabled email sending.
     123             :       //
     124           0 :       logger.atFine().log(
     125             :           "Not sending '%s': Email sending is disabled by server config", messageClass);
     126           0 :       return;
     127             :     }
     128             : 
     129         106 :     if (!notify.shouldNotify()) {
     130          10 :       logger.atFine().log("Not sending '%s': Notify handling is NONE", messageClass);
     131          10 :       return;
     132             :     }
     133             : 
     134         106 :     init();
     135         106 :     if (messageId == null) {
     136           0 :       throw new IllegalStateException("All emails must have a messageId");
     137             :     }
     138         106 :     format();
     139         106 :     appendText(textTemplate("Footer"));
     140         106 :     if (useHtml()) {
     141         106 :       appendHtml(soyHtmlTemplate("FooterHtml"));
     142             :     }
     143             : 
     144         106 :     Set<Address> smtpRcptToPlaintextOnly = new HashSet<>();
     145         106 :     if (shouldSendMessage()) {
     146          55 :       if (fromId != null) {
     147          50 :         Optional<AccountState> fromUser = args.accountCache.get(fromId);
     148          50 :         if (fromUser.isPresent()) {
     149          50 :           GeneralPreferencesInfo senderPrefs = fromUser.get().generalPreferences();
     150          50 :           CurrentUser user = args.currentUserProvider.get();
     151          50 :           boolean isImpersonating = user.isIdentifiedUser() && user.isImpersonating();
     152          50 :           if (isImpersonating && user.getAccountId() != fromId) {
     153             :             // This should not be possible, if this is the case it means the RequestContext is not
     154             :             // set up correctly.
     155           0 :             throw new EmailException(
     156           0 :                 String.format(
     157             :                     "User %s is sending email from %s, while acting on behalf of %s",
     158           0 :                     user.asIdentifiedUser().getRealUser().getAccountId(),
     159             :                     fromId,
     160           0 :                     user.getAccountId()));
     161             :           }
     162          50 :           if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
     163             :             // Include the sender in email if they enabled email notifications on their own
     164             :             // comments.
     165             :             //
     166           1 :             logger.atFine().log(
     167             :                 "CC email sender %s because the email strategy of this user is %s",
     168           1 :                 fromUser.get().account().id(), CC_ON_OWN_COMMENTS);
     169           1 :             add(RecipientType.CC, fromId);
     170          50 :           } else if (isImpersonating) {
     171             :             // If we are impersonating a user, make sure they receive a CC of
     172             :             // this message regardless of email strategy, unless email notifications are explicitly
     173             :             // disabled for this user. This way they can always review and audit what we sent
     174             :             // on their behalf to others.
     175           2 :             logger.atFine().log(
     176             :                 "CC email sender %s because the email is sent on behalf of and email notifications"
     177             :                     + " are enabled for this user.",
     178           2 :                 fromUser.get().account().id());
     179           2 :             add(RecipientType.CC, fromId);
     180             : 
     181          50 :           } else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) {
     182             :             // If they don't want a copy, but we queued one up anyway,
     183             :             // drop them from the recipient lists, but only if the user is not being impersonated.
     184             :             //
     185          47 :             logger.atFine().log(
     186             :                 "Not CCing email sender %s because the email strategy of this user is not %s but"
     187             :                     + " %s",
     188          47 :                 fromUser.get().account().id(),
     189             :                 CC_ON_OWN_COMMENTS,
     190          47 :                 senderPrefs != null ? senderPrefs.getEmailStrategy() : null);
     191          47 :             removeUser(fromUser.get().account());
     192             :           }
     193             :         }
     194             :       }
     195             :       // Check the preferences of all recipients. If any user has disabled
     196             :       // his email notifications then drop him from recipients' list.
     197             :       // In addition, check if users only want to receive plaintext email.
     198          55 :       for (Account.Id id : rcptTo) {
     199          54 :         Optional<AccountState> thisUser = args.accountCache.get(id);
     200          54 :         if (thisUser.isPresent()) {
     201          54 :           Account thisUserAccount = thisUser.get().account();
     202          54 :           GeneralPreferencesInfo prefs = thisUser.get().generalPreferences();
     203          54 :           if (prefs == null || prefs.getEmailStrategy() == DISABLED) {
     204           1 :             logger.atFine().log(
     205             :                 "Not emailing account %s because user has set email strategy to %s", id, DISABLED);
     206           1 :             removeUser(thisUserAccount);
     207          54 :           } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) {
     208           2 :             logger.atFine().log(
     209             :                 "Removing account %s from HTML email because user prefers plain text emails", id);
     210           2 :             removeUser(thisUserAccount);
     211           2 :             smtpRcptToPlaintextOnly.add(
     212           2 :                 Address.create(thisUserAccount.fullName(), thisUserAccount.preferredEmail()));
     213             :           }
     214             :         }
     215          54 :         if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) {
     216           7 :           logger.atFine().log("Not sending '%s': No SMTP recipients", messageClass);
     217           7 :           return;
     218             :         }
     219          54 :       }
     220             : 
     221             :       // Set Reply-To only if it hasn't been set by a child class
     222             :       // Reply-To will already be populated for the message types where Gerrit supports
     223             :       // inbound email replies.
     224          55 :       if (!headers.containsKey(FieldName.REPLY_TO)) {
     225          55 :         StringJoiner j = new StringJoiner(", ");
     226          55 :         if (fromId != null) {
     227          50 :           Address address = toAddress(fromId);
     228          50 :           if (address != null) {
     229          50 :             j.add(address.email());
     230             :           }
     231             :         }
     232             :         // For users who prefer plaintext, this comes at the cost of not being
     233             :         // listed in the multipart To and Cc headers. We work around this by adding
     234             :         // all users to the Reply-To address in both the plaintext and multipart
     235             :         // email. We should exclude any BCC addresses from reply-to, because they should be
     236             :         // invisible to other recipients.
     237          55 :         Sets.difference(Sets.union(smtpRcptTo, smtpRcptToPlaintextOnly), smtpBccRcptTo).stream()
     238          55 :             .forEach(a -> j.add(a.email()));
     239          55 :         setHeader(FieldName.REPLY_TO, j.toString());
     240             :       }
     241             : 
     242          55 :       String textPart = textBody.toString();
     243          55 :       OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args();
     244          55 :       va.messageClass = messageClass;
     245          55 :       va.smtpFromAddress = smtpFromAddress;
     246          55 :       va.smtpRcptTo = smtpRcptTo;
     247          55 :       va.headers = headers;
     248          55 :       va.body = textPart;
     249             : 
     250          55 :       if (useHtml()) {
     251          55 :         va.htmlBody = htmlBody.toString();
     252             :       } else {
     253           0 :         va.htmlBody = null;
     254             :       }
     255             : 
     256          55 :       Set<Address> intersection = Sets.intersection(va.smtpRcptTo, smtpRcptToPlaintextOnly);
     257          55 :       if (!intersection.isEmpty()) {
     258           0 :         logger.atSevere().log("Email '%s' will be sent twice to %s", messageClass, intersection);
     259             :       }
     260          55 :       if (!va.smtpRcptTo.isEmpty()) {
     261             :         // Send multipart message
     262          55 :         addMessageId(va, "-HTML");
     263          55 :         if (!validateEmail(va)) return;
     264          55 :         logger.atFine().log(
     265             :             "Sending multipart '%s' from %s to %s",
     266             :             messageClass, va.smtpFromAddress, va.smtpRcptTo);
     267          55 :         args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody);
     268             :       }
     269          55 :       if (!smtpRcptToPlaintextOnly.isEmpty()) {
     270           2 :         addMessageId(va, "-PLAIN");
     271             :         // Send plaintext message
     272           2 :         Map<String, EmailHeader> shallowCopy = new HashMap<>();
     273           2 :         shallowCopy.putAll(headers);
     274             :         // Remove To and Cc
     275           2 :         shallowCopy.remove(FieldName.TO);
     276           2 :         shallowCopy.remove(FieldName.CC);
     277           2 :         for (Address a : smtpRcptToPlaintextOnly) {
     278             :           // Add new To
     279           2 :           EmailHeader.AddressList to = new EmailHeader.AddressList();
     280           2 :           to.add(a);
     281           2 :           shallowCopy.put(FieldName.TO, to);
     282           2 :         }
     283           2 :         if (!validateEmail(va)) return;
     284           2 :         logger.atFine().log(
     285             :             "Sending plaintext '%s' from %s to %s",
     286             :             messageClass, va.smtpFromAddress, smtpRcptToPlaintextOnly);
     287           2 :         args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body);
     288             :       }
     289             :     }
     290         106 :   }
     291             : 
     292             :   private boolean validateEmail(OutgoingEmailValidationListener.Args va) {
     293          55 :     for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
     294             :       try {
     295           0 :         validator.validateOutgoingEmail(va);
     296           0 :       } catch (ValidationException e) {
     297           0 :         logger.atFine().log(
     298             :             "Not sending '%s': Rejected by outgoing email validator: %s",
     299           0 :             messageClass, e.getMessage());
     300           0 :         return false;
     301           0 :       }
     302           0 :     }
     303          55 :     return true;
     304             :   }
     305             : 
     306             :   // All message ids must start with < and end with >. Also, they must have @domain and no spaces.
     307             :   private void addMessageId(OutgoingEmailValidationListener.Args va, String suffix) {
     308          55 :     if (messageId != null) {
     309          55 :       String message = "<" + messageId.id() + suffix + "@" + getGerritHost() + ">";
     310          55 :       message = message.replaceAll("\\s", "");
     311          55 :       va.headers.put(FieldName.MESSAGE_ID, new StringEmailHeader(message));
     312             :     }
     313          55 :   }
     314             : 
     315             :   /** Format the message body by calling {@link #appendText(String)}. */
     316             :   protected abstract void format() throws EmailException;
     317             : 
     318             :   /**
     319             :    * Setup the message headers and envelope (TO, CC, BCC).
     320             :    *
     321             :    * @throws EmailException if an error occurred.
     322             :    */
     323             :   protected void init() throws EmailException {
     324         106 :     setupSoyContext();
     325             : 
     326         106 :     smtpFromAddress = args.fromAddressGenerator.get().from(fromId);
     327         106 :     setHeader(FieldName.DATE, Instant.now());
     328         106 :     headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
     329         106 :     headers.put(FieldName.TO, new EmailHeader.AddressList());
     330         106 :     headers.put(FieldName.CC, new EmailHeader.AddressList());
     331         106 :     setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
     332             : 
     333         106 :     for (RecipientType recipientType : notify.accounts().keySet()) {
     334           8 :       notify.accounts().get(recipientType).stream().forEach(a -> add(recipientType, a));
     335           8 :     }
     336             : 
     337         106 :     setHeader(MailHeader.MESSAGE_TYPE.fieldName(), messageClass);
     338         106 :     footers.add(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
     339         106 :     textBody = new StringBuilder();
     340         106 :     htmlBody = new StringBuilder();
     341             : 
     342         106 :     if (fromId != null && args.fromAddressGenerator.get().isGenericAddress(fromId)) {
     343           0 :       appendText(getFromLine());
     344             :     }
     345         106 :   }
     346             : 
     347             :   protected String getFromLine() {
     348           0 :     StringBuilder f = new StringBuilder();
     349           0 :     Optional<Account> account = args.accountCache.get(fromId).map(AccountState::account);
     350           0 :     if (account.isPresent()) {
     351           0 :       String name = account.get().fullName();
     352           0 :       String email = account.get().preferredEmail();
     353           0 :       if ((name != null && !name.isEmpty()) || (email != null && !email.isEmpty())) {
     354           0 :         f.append("From");
     355           0 :         if (name != null && !name.isEmpty()) {
     356           0 :           f.append(" ").append(name);
     357             :         }
     358           0 :         if (email != null && !email.isEmpty()) {
     359           0 :           f.append(" <").append(email).append(">");
     360             :         }
     361           0 :         f.append(":\n\n");
     362             :       }
     363             :     }
     364           0 :     return f.toString();
     365             :   }
     366             : 
     367             :   public String getGerritHost() {
     368         106 :     if (getGerritUrl() != null) {
     369             :       try {
     370         106 :         return new URL(getGerritUrl()).getHost();
     371           0 :       } catch (MalformedURLException e) {
     372             :         // Try something else.
     373             :       }
     374             :     }
     375             : 
     376             :     // Fall back onto whatever the local operating system thinks
     377             :     // this server is called. We hopefully didn't get here as a
     378             :     // good admin would have configured the canonical url.
     379             :     //
     380           0 :     return SystemReader.getInstance().getHostname();
     381             :   }
     382             : 
     383             :   @Nullable
     384             :   public String getSettingsUrl() {
     385         106 :     return args.urlFormatter.get().getSettingsUrl().orElse(null);
     386             :   }
     387             : 
     388             :   @Nullable
     389             :   private String getGerritUrl() {
     390         106 :     return args.urlFormatter.get().getWebUrl().orElse(null);
     391             :   }
     392             : 
     393             :   /** Set a header in the outgoing message. */
     394             :   protected void setHeader(String name, String value) {
     395         106 :     headers.put(name, new StringEmailHeader(value));
     396         106 :   }
     397             : 
     398             :   /** Remove a header from the outgoing message. */
     399             :   protected void removeHeader(String name) {
     400           0 :     headers.remove(name);
     401           0 :   }
     402             : 
     403             :   protected void setHeader(String name, Instant date) {
     404         106 :     headers.put(name, new EmailHeader.Date(date));
     405         106 :   }
     406             : 
     407             :   /** Append text to the outgoing email body. */
     408             :   protected void appendText(String text) {
     409         106 :     if (text != null) {
     410         106 :       textBody.append(text);
     411             :     }
     412         106 :   }
     413             : 
     414             :   /** Append html to the outgoing email body. */
     415             :   protected void appendHtml(String html) {
     416         106 :     if (html != null) {
     417         106 :       htmlBody.append(html);
     418             :     }
     419         106 :   }
     420             : 
     421             :   /** Lookup a human readable name for an account, usually the "full name". */
     422             :   protected String getNameFor(@Nullable Account.Id accountId) {
     423         103 :     if (accountId == null) {
     424           1 :       return args.gerritPersonIdent.get().getName();
     425             :     }
     426             : 
     427         103 :     Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account);
     428         103 :     String name = null;
     429         103 :     if (account.isPresent()) {
     430         103 :       name = account.get().fullName();
     431         103 :       if (name == null) {
     432          13 :         name = account.get().preferredEmail();
     433             :       }
     434             :     }
     435         103 :     if (name == null) {
     436          11 :       name = args.anonymousCowardName + " #" + accountId;
     437             :     }
     438         103 :     return name;
     439             :   }
     440             : 
     441             :   /**
     442             :    * Gets the human readable name and email for an account; if neither are available, returns the
     443             :    * Anonymous Coward name.
     444             :    *
     445             :    * @param accountId user to fetch.
     446             :    * @return name/email of account, or Anonymous Coward if unset.
     447             :    */
     448             :   protected String getNameEmailFor(@Nullable Account.Id accountId) {
     449         103 :     if (accountId == null) {
     450           1 :       PersonIdent gerritIdent = args.gerritPersonIdent.get();
     451           1 :       return gerritIdent.getName() + " <" + gerritIdent.getEmailAddress() + ">";
     452             :     }
     453             : 
     454         103 :     Optional<Account> account = args.accountCache.get(accountId).map(AccountState::account);
     455         103 :     if (account.isPresent()) {
     456         103 :       String name = account.get().fullName();
     457         103 :       String email = account.get().preferredEmail();
     458         103 :       if (name != null && email != null) {
     459          97 :         return name + " <" + email + ">";
     460          13 :       } else if (name != null) {
     461           5 :         return name;
     462          13 :       } else if (email != null) {
     463           6 :         return email;
     464             :       }
     465             :     }
     466          11 :     return args.anonymousCowardName + " #" + accountId;
     467             :   }
     468             : 
     469             :   /**
     470             :    * Gets the human readable name and email for an account; if both are unavailable, returns the
     471             :    * username. If no username is set, this function returns null.
     472             :    *
     473             :    * @param accountId user to fetch.
     474             :    * @return name/email of account, username, or null if unset or the accountId is null.
     475             :    */
     476             :   @Nullable
     477             :   protected String getUserNameEmailFor(@Nullable Account.Id accountId) {
     478           9 :     if (accountId == null) {
     479           0 :       return null;
     480             :     }
     481             : 
     482           9 :     Optional<AccountState> accountState = args.accountCache.get(accountId);
     483           9 :     if (!accountState.isPresent()) {
     484           0 :       return null;
     485             :     }
     486             : 
     487           9 :     Account account = accountState.get().account();
     488           9 :     String name = account.fullName();
     489           9 :     String email = account.preferredEmail();
     490           9 :     if (name != null && email != null) {
     491           9 :       return name + " <" + email + ">";
     492           2 :     } else if (email != null) {
     493           1 :       return email;
     494           1 :     } else if (name != null) {
     495           0 :       return name;
     496             :     }
     497           1 :     return accountState.get().userName().orElse(null);
     498             :   }
     499             : 
     500             :   protected boolean shouldSendMessage() {
     501         104 :     if (textBody.length() == 0) {
     502             :       // If we have no message body, don't send.
     503           0 :       logger.atFine().log("Not sending '%s': No message body", messageClass);
     504           0 :       return false;
     505             :     }
     506             : 
     507         104 :     if (smtpRcptTo.isEmpty()) {
     508             :       // If we have nobody to send this message to, then all of our
     509             :       // selection filters previously for this type of message were
     510             :       // unable to match a destination. Don't bother sending it.
     511          11 :       logger.atFine().log("Not sending '%s': No recipients", messageClass);
     512          11 :       return false;
     513             :     }
     514             : 
     515         102 :     if (notify.accounts().isEmpty()
     516         102 :         && smtpRcptTo.size() == 1
     517         102 :         && rcptTo.size() == 1
     518         101 :         && rcptTo.contains(fromId)) {
     519             :       // If the only recipient is also the sender, don't bother.
     520             :       //
     521         101 :       logger.atFine().log("Not sending '%s': Sender is only recipient", messageClass);
     522         101 :       return false;
     523             :     }
     524             : 
     525          51 :     return true;
     526             :   }
     527             : 
     528             :   /** Schedule this message for delivery to the listed address. */
     529             :   protected final void addByEmail(RecipientType rt, Collection<Address> list) {
     530         103 :     addByEmail(rt, list, false);
     531         103 :   }
     532             : 
     533             :   /** Schedule this message for delivery to the listed address. */
     534             :   protected final void addByEmail(RecipientType rt, Collection<Address> list, boolean override) {
     535         103 :     for (final Address id : list) {
     536          10 :       add(rt, id, override);
     537          10 :     }
     538         103 :   }
     539             : 
     540             :   /** Schedule delivery of this message to the given account. */
     541             :   protected void add(RecipientType rt, Account.Id to) {
     542         105 :     add(rt, to, false);
     543         105 :   }
     544             : 
     545             :   protected void add(RecipientType rt, Account.Id to, boolean override) {
     546             :     try {
     547         105 :       if (!rcptTo.contains(to) && isVisibleTo(to)) {
     548         105 :         rcptTo.add(to);
     549         105 :         add(rt, toAddress(to), override);
     550             :       }
     551           0 :     } catch (PermissionBackendException e) {
     552           0 :       logger.atSevere().withCause(e).log("Error reading database for account: %s", to);
     553         105 :     }
     554         105 :   }
     555             : 
     556             :   /**
     557             :    * Returns whether this email is visible to the given account
     558             :    *
     559             :    * @param to account.
     560             :    * @throws PermissionBackendException thrown if checking a permission fails due to an error in the
     561             :    *     permission backend
     562             :    */
     563             :   protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
     564           9 :     return true;
     565             :   }
     566             : 
     567             :   /** Schedule delivery of this message to the given account. */
     568             :   protected final void add(RecipientType rt, Address addr) {
     569          15 :     add(rt, addr, false);
     570          15 :   }
     571             : 
     572             :   protected final void add(RecipientType rt, Address addr, boolean override) {
     573         106 :     if (addr != null && addr.email() != null && addr.email().length() > 0) {
     574         104 :       if (!args.validator.isValid(addr.email())) {
     575           5 :         logger.atWarning().log("Not emailing %s (invalid email address)", addr.email());
     576         104 :       } else if (args.emailSender.canEmail(addr.email())) {
     577         104 :         if (!smtpRcptTo.add(addr)) {
     578          10 :           if (!override) {
     579          10 :             return;
     580             :           }
     581           9 :           ((EmailHeader.AddressList) headers.get(FieldName.TO)).remove(addr.email());
     582           9 :           ((EmailHeader.AddressList) headers.get(FieldName.CC)).remove(addr.email());
     583           9 :           smtpBccRcptTo.remove(addr);
     584             :         }
     585         104 :         switch (rt) {
     586             :           case TO:
     587          88 :             ((EmailHeader.AddressList) headers.get(FieldName.TO)).add(addr);
     588          88 :             break;
     589             :           case CC:
     590         101 :             ((EmailHeader.AddressList) headers.get(FieldName.CC)).add(addr);
     591         101 :             break;
     592             :           case BCC:
     593          11 :             smtpBccRcptTo.add(addr);
     594             :             break;
     595             :         }
     596             :       }
     597             :     }
     598         106 :   }
     599             : 
     600             :   @Nullable
     601             :   private Address toAddress(Account.Id id) {
     602         105 :     Optional<Account> accountState = args.accountCache.get(id).map(AccountState::account);
     603         105 :     if (!accountState.isPresent()) {
     604           0 :       return null;
     605             :     }
     606             : 
     607         105 :     Account account = accountState.get();
     608         105 :     String e = account.preferredEmail();
     609         105 :     if (!account.isActive() || e == null) {
     610          11 :       return null;
     611             :     }
     612         103 :     return Address.create(account.fullName(), e);
     613             :   }
     614             : 
     615             :   protected void setupSoyContext() {
     616         106 :     soyContext = new HashMap<>();
     617         106 :     footers = new ArrayList<>();
     618             : 
     619         106 :     soyContext.put("messageClass", messageClass);
     620         106 :     soyContext.put("footers", footers);
     621             : 
     622         106 :     soyContextEmailData = new HashMap<>();
     623         106 :     soyContextEmailData.put("settingsUrl", getSettingsUrl());
     624         106 :     soyContextEmailData.put("instanceName", getInstanceName());
     625         106 :     soyContextEmailData.put("gerritHost", getGerritHost());
     626         106 :     soyContextEmailData.put("gerritUrl", getGerritUrl());
     627         106 :     soyContext.put("email", soyContextEmailData);
     628         106 :   }
     629             : 
     630             :   private String getInstanceName() {
     631         106 :     return args.instanceNameProvider.get();
     632             :   }
     633             : 
     634             :   /** Renders a soy template of kind="text". */
     635             :   protected String textTemplate(String name) {
     636         106 :     return configureRenderer(name).renderText().get();
     637             :   }
     638             : 
     639             :   /** Renders a soy template of kind="html". */
     640             :   protected String soyHtmlTemplate(String name) {
     641         106 :     return configureRenderer(name).renderHtml().get().toString();
     642             :   }
     643             : 
     644             :   /** Configures a soy renderer for the given template name and rendering data map. */
     645             :   private SoySauce.Renderer configureRenderer(String templateName) {
     646         106 :     int baseNameIndex = templateName.indexOf("_");
     647             :     // In case there are multiple templates in file (now only InboundEmailRejection and
     648             :     // InboundEmailRejectionHtml).
     649             :     String fileNamespace =
     650         106 :         baseNameIndex == -1 ? templateName : templateName.substring(0, baseNameIndex);
     651         106 :     String templateInFileNamespace =
     652         106 :         String.join(".", SOY_TEMPLATE_NAMESPACE, fileNamespace, templateName);
     653         106 :     String templateInCommonNamespace = String.join(".", SOY_TEMPLATE_NAMESPACE, templateName);
     654         106 :     SoySauce soySauce = args.soySauce.get();
     655             :     // For backwards compatibility with existing customizations and plugin templates with the
     656             :     // old non-unique namespace.
     657             :     String fullTemplateName =
     658         106 :         soySauce.hasTemplate(templateInFileNamespace)
     659         106 :             ? templateInFileNamespace
     660         106 :             : templateInCommonNamespace;
     661         106 :     return soySauce.renderTemplate(fullTemplateName).setData(soyContext);
     662             :   }
     663             : 
     664             :   protected void removeUser(Account user) {
     665          47 :     String fromEmail = user.preferredEmail();
     666          47 :     for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext(); ) {
     667          47 :       if (j.next().email().equals(fromEmail)) {
     668          47 :         j.remove();
     669             :       }
     670             :     }
     671          47 :     for (Map.Entry<String, EmailHeader> entry : headers.entrySet()) {
     672             :       // Don't remove fromEmail from the "From" header though!
     673          47 :       if (entry.getValue() instanceof AddressList && !entry.getKey().equals("From")) {
     674          47 :         ((AddressList) entry.getValue()).remove(fromEmail);
     675             :       }
     676          47 :     }
     677          47 :   }
     678             : 
     679             :   protected final boolean useHtml() {
     680         106 :     return args.settings.html;
     681             :   }
     682             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750