All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.mail.send.OutgoingEmail Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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.common.base.Preconditions.checkNotNull;
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.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
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.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gwtorm.server.OrmException;
import com.google.template.soy.data.SanitizedContent;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
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.Set;
import java.util.StringJoiner;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.context.InternalContextAdapterImpl;
import org.apache.velocity.runtime.RuntimeInstance;
import org.apache.velocity.runtime.parser.node.SimpleNode;
import org.eclipse.jgit.util.SystemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Sends an email to one or more interested parties. */
public abstract class OutgoingEmail {
  private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);

  private static final String HDR_TO = "To";
  private static final String HDR_CC = "CC";

  protected String messageClass;
  private final HashSet rcptTo = new HashSet<>();
  private final Map headers;
  private final Set
smtpRcptTo = new HashSet<>(); private Address smtpFromAddress; private StringBuilder textBody; private StringBuilder htmlBody; private ListMultimap accountsToNotify = ImmutableListMultimap.of(); protected VelocityContext velocityContext; protected Map soyContext; protected Map soyContextEmailData; protected List footers; protected final EmailArguments args; protected Account.Id fromId; protected NotifyHandling notify = NotifyHandling.ALL; protected OutgoingEmail(EmailArguments ea, String mc) { args = ea; messageClass = mc; headers = new LinkedHashMap<>(); } public void setFrom(final Account.Id id) { fromId = id; } public void setNotify(NotifyHandling notify) { this.notify = checkNotNull(notify); } public void setAccountsToNotify(ListMultimap accountsToNotify) { this.accountsToNotify = checkNotNull(accountsToNotify); } /** * Format and enqueue the message for delivery. * * @throws EmailException */ public void send() throws EmailException { if (NotifyHandling.NONE.equals(notify) && accountsToNotify.isEmpty()) { return; } if (!args.emailSender.isEnabled()) { // Server has explicitly disabled email sending. // return; } init(); if (useHtml()) { appendHtml(soyHtmlTemplate("HeaderHtml")); } format(); appendText(textTemplate("Footer")); if (useHtml()) { appendHtml(soyHtmlTemplate("FooterHtml")); } Set
smtpRcptToPlaintextOnly = new HashSet<>(); if (shouldSendMessage()) { if (fromId != null) { final Account fromUser = args.accountCache.get(fromId).getAccount(); GeneralPreferencesInfo senderPrefs = fromUser.getGeneralPreferencesInfo(); if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) { // If we are impersonating a user, make sure they receive a CC of // this message so they can always review and audit what we sent // on their behalf to others. // add(RecipientType.CC, fromId); } else if (!accountsToNotify.containsValue(fromId) && rcptTo.remove(fromId)) { // If they don't want a copy, but we queued one up anyway, // drop them from the recipient lists. // removeUser(fromUser); } } // 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) { Account thisUser = args.accountCache.get(id).getAccount(); GeneralPreferencesInfo prefs = thisUser.getGeneralPreferencesInfo(); if (prefs == null || prefs.getEmailStrategy() == DISABLED) { removeUser(thisUser); } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) { removeUser(thisUser); smtpRcptToPlaintextOnly.add( new Address(thisUser.getFullName(), thisUser.getPreferredEmail())); } if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) { 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("Reply-To")) { StringJoiner j = new StringJoiner(", "); if (fromId != null) { Address address = toAddress(fromId); if (address != null) { j.add(address.getEmail()); } } smtpRcptTo.stream().forEach(a -> j.add(a.getEmail())); smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.getEmail())); setHeader("Reply-To", j.toString()); } String textPart = textBody.toString(); OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args(); va.messageClass = messageClass; va.smtpFromAddress = smtpFromAddress; va.smtpRcptTo = smtpRcptTo; va.headers = headers; va.body = textPart; if (useHtml()) { va.htmlBody = htmlBody.toString(); } else { va.htmlBody = null; } for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) { try { validator.validateOutgoingEmail(va); } catch (ValidationException e) { return; } } if (!smtpRcptTo.isEmpty()) { // Send multipart message args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody); } if (!smtpRcptToPlaintextOnly.isEmpty()) { // Send plaintext message Map shallowCopy = new HashMap<>(); shallowCopy.putAll(headers); // Remove To and Cc shallowCopy.remove(HDR_TO); shallowCopy.remove(HDR_CC); for (Address a : smtpRcptToPlaintextOnly) { // Add new To EmailHeader.AddressList to = new EmailHeader.AddressList(); to.add(a); shallowCopy.put(HDR_TO, to); } args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body); } } } /** Format the message body by calling {@link #appendText(String)}. */ protected abstract void format() throws EmailException; /** * Setup the message headers and envelope (TO, CC, BCC). * * @throws EmailException if an error occurred. */ protected void init() throws EmailException { setupVelocityContext(); setupSoyContext(); smtpFromAddress = args.fromAddressGenerator.from(fromId); setHeader("Date", new Date()); headers.put("From", new EmailHeader.AddressList(smtpFromAddress)); headers.put(HDR_TO, new EmailHeader.AddressList()); headers.put(HDR_CC, new EmailHeader.AddressList()); setHeader("Message-ID", ""); for (RecipientType recipientType : accountsToNotify.keySet()) { add(recipientType, accountsToNotify.get(recipientType)); } setHeader("X-Gerrit-MessageType", messageClass); textBody = new StringBuilder(); htmlBody = new StringBuilder(); if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) { appendText(getFromLine()); } } protected String getFromLine() { final Account account = args.accountCache.get(fromId).getAccount(); final String name = account.getFullName(); final String email = account.getPreferredEmail(); StringBuilder f = new StringBuilder(); 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() { if (getGerritUrl() != null) { try { return new URL(getGerritUrl()).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(); } public String getSettingsUrl() { if (getGerritUrl() != null) { final StringBuilder r = new StringBuilder(); r.append(getGerritUrl()); r.append("settings"); return r.toString(); } return null; } public String getGerritUrl() { return args.urlProvider.get(); } /** Set a header in the outgoing message using a template. */ protected void setVHeader(final String name, final String value) throws EmailException { setHeader(name, velocify(value)); } /** Set a header in the outgoing message. */ protected void setHeader(final String name, final String value) { headers.put(name, new EmailHeader.String(value)); } /** Remove a header from the outgoing message. */ protected void removeHeader(final String name) { headers.remove(name); } protected void setHeader(final String name, final Date date) { headers.put(name, new EmailHeader.Date(date)); } /** Append text to the outgoing email body. */ protected void appendText(final String text) { if (text != null) { textBody.append(text); } } /** Append html to the outgoing email body. */ protected void appendHtml(String html) { if (html != null) { htmlBody.append(html); } } /** * Gets the human readable name for an account, usually the "full name". * * @param accountId user to fetch. * @return name of the account, or the server identity name if null. */ protected String getNameFor(@Nullable Account.Id accountId) { if (accountId == null) { return args.gerritPersonIdent.getName(); } return args.accountCache.get(accountId).getAccount().getName(args.anonymousCowardName); } /** * Gets the human readable name and email for an account. * * @param accountId user to fetch. * @return name/email of account; Anonymous Coward if unset or the server identity if null. */ protected String getNameEmailFor(@Nullable Account.Id accountId) { if (accountId == null) { return String.format( "%s <%s>", args.gerritPersonIdent.getName(), args.gerritPersonIdent.getEmailAddress()); } return args.accountCache.get(accountId).getAccount().getNameEmail(args.anonymousCowardName); } /** * 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. */ @Nullable protected String getUserNameEmailFor(@Nullable Account.Id accountId) { if (accountId == null) { return null; } AccountState who = args.accountCache.get(accountId); String name = who.getAccount().getFullName(); String email = who.getAccount().getPreferredEmail(); if (name != null && email != null) { return name + " <" + email + ">"; } else if (email != null) { return email; } else if (name != null) { return name; } String username = who.getUserName(); if (username != null) { return username; } return null; } protected boolean shouldSendMessage() { if (textBody.length() == 0) { // If we have no message body, don't send. 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. return false; } if ((accountsToNotify == null || accountsToNotify.isEmpty()) && smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) { // If the only recipient is also the sender, don't bother. // return false; } return true; } /** Schedule this message for delivery to the listed accounts. */ protected void add(final RecipientType rt, final Collection list) { for (final Account.Id id : list) { add(rt, id); } } protected void add(final RecipientType rt, final UserIdentity who) { if (who != null && who.getAccount() != null) { add(rt, who.getAccount()); } } /** Schedule delivery of this message to the given account. */ protected void add(final RecipientType rt, final Account.Id to) { try { if (!rcptTo.contains(to) && isVisibleTo(to)) { rcptTo.add(to); add(rt, toAddress(to)); } } catch (OrmException e) { log.error("Error reading database for account: " + to, e); } } /** * @param to account. * @throws OrmException * @return whether this email is visible to the given account. */ protected boolean isVisibleTo(final Account.Id to) throws OrmException { return true; } /** Schedule delivery of this message to the given account. */ protected void add(final RecipientType rt, final Address addr) { if (addr != null && addr.getEmail() != null && addr.getEmail().length() > 0) { if (!OutgoingEmailValidator.isValid(addr.getEmail())) { log.warn("Not emailing " + addr.getEmail() + " (invalid email address)"); } else if (!args.emailSender.canEmail(addr.getEmail())) { log.warn("Not emailing " + addr.getEmail() + " (prohibited by allowrcpt)"); } else if (smtpRcptTo.add(addr)) { switch (rt) { case TO: ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr); break; case CC: ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr); break; case BCC: break; } } } } private Address toAddress(final Account.Id id) { final Account a = args.accountCache.get(id).getAccount(); final String e = a.getPreferredEmail(); if (!a.isActive() || e == null) { return null; } return new Address(a.getFullName(), e); } protected void setupVelocityContext() { velocityContext = new VelocityContext(); velocityContext.put("email", this); velocityContext.put("messageClass", messageClass); velocityContext.put("StringUtils", StringUtils.class); } protected void setupSoyContext() { soyContext = new HashMap<>(); footers = new ArrayList<>(); soyContext.put("messageClass", messageClass); soyContext.put("footers", footers); soyContextEmailData = new HashMap<>(); soyContextEmailData.put("settingsUrl", getSettingsUrl()); soyContextEmailData.put("gerritHost", getGerritHost()); soyContextEmailData.put("gerritUrl", getGerritUrl()); soyContext.put("email", soyContextEmailData); } protected String velocify(String template) throws EmailException { try { RuntimeInstance runtime = args.velocityRuntime; String templateName = "OutgoingEmail"; SimpleNode tree = runtime.parse(new StringReader(template), templateName); InternalContextAdapterImpl ica = new InternalContextAdapterImpl(velocityContext); ica.pushCurrentTemplateName(templateName); try { tree.init(ica, runtime); StringWriter w = new StringWriter(); tree.render(ica, w); return w.toString(); } finally { ica.popCurrentTemplateName(); } } catch (Exception e) { throw new EmailException("Cannot format velocity template: " + template, e); } } protected String velocifyFile(String name) throws EmailException { try { RuntimeInstance runtime = args.velocityRuntime; if (runtime.getLoaderNameForResource(name) == null) { name = "com/google/gerrit/server/mail/" + name; } Template template = runtime.getTemplate(name, UTF_8.name()); StringWriter w = new StringWriter(); template.merge(velocityContext, w); return w.toString(); } catch (Exception e) { throw new EmailException("Cannot format velocity template " + name, e); } } private String soyTemplate(String name, SanitizedContent.ContentKind kind) { return args.soyTofu .newRenderer("com.google.gerrit.server.mail.template." + name) .setContentKind(kind) .setData(soyContext) .render(); } protected String soyTextTemplate(String name) { return soyTemplate(name, SanitizedContent.ContentKind.TEXT); } protected String soyHtmlTemplate(String name) { return soyTemplate(name, SanitizedContent.ContentKind.HTML); } /** * Evaluate the named template according to the following priority: 1) Velocity file override, * OR... 2) Soy file override, OR... 3) Soy resource. */ protected String textTemplate(String name) throws EmailException { String velocityName = name + ".vm"; Path filePath = args.site.mail_dir.resolve(velocityName); if (Files.isRegularFile(filePath)) { return velocifyFile(velocityName); } return soyTextTemplate(name); } protected void removeUser(Account user) { String fromEmail = user.getPreferredEmail(); for (Iterator
j = smtpRcptTo.iterator(); j.hasNext(); ) { if (j.next().getEmail().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); } } } protected final boolean useHtml() { return args.settings.html && supportsHtml(); } /** Override this method to enable HTML in a subclass. */ protected boolean supportsHtml() { return false; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy