PageRenderTime 83ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/java/com/google/gerrit/server/mail/send/OutgoingEmail.java

https://gitlab.com/chenfengxu/gerrit
Java | 617 lines | 458 code | 72 blank | 87 comment | 138 complexity | b8944508535832bae7fa34360c56d57f MD5 | raw file
  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. package com.google.gerrit.server.mail.send;
  15. import static com.google.common.base.Preconditions.checkNotNull;
  16. import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS;
  17. import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
  18. import com.google.common.collect.ImmutableListMultimap;
  19. import com.google.common.collect.ListMultimap;
  20. import com.google.gerrit.common.errors.EmailException;
  21. import com.google.gerrit.extensions.api.changes.NotifyHandling;
  22. import com.google.gerrit.extensions.api.changes.RecipientType;
  23. import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
  24. import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
  25. import com.google.gerrit.reviewdb.client.Account;
  26. import com.google.gerrit.reviewdb.client.UserIdentity;
  27. import com.google.gerrit.server.account.AccountState;
  28. import com.google.gerrit.server.mail.Address;
  29. import com.google.gerrit.server.mail.MailHeader;
  30. import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
  31. import com.google.gerrit.server.permissions.PermissionBackendException;
  32. import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
  33. import com.google.gerrit.server.validators.ValidationException;
  34. import com.google.template.soy.data.SanitizedContent;
  35. import java.net.MalformedURLException;
  36. import java.net.URL;
  37. import java.util.ArrayList;
  38. import java.util.Collection;
  39. import java.util.Date;
  40. import java.util.HashMap;
  41. import java.util.HashSet;
  42. import java.util.Iterator;
  43. import java.util.LinkedHashMap;
  44. import java.util.List;
  45. import java.util.Map;
  46. import java.util.Optional;
  47. import java.util.Set;
  48. import java.util.StringJoiner;
  49. import org.apache.james.mime4j.dom.field.FieldName;
  50. import org.eclipse.jgit.util.SystemReader;
  51. import org.slf4j.Logger;
  52. import org.slf4j.LoggerFactory;
  53. /** Sends an email to one or more interested parties. */
  54. public abstract class OutgoingEmail {
  55. private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
  56. protected String messageClass;
  57. private final Set<Account.Id> rcptTo = new HashSet<>();
  58. private final Map<String, EmailHeader> headers;
  59. private final Set<Address> smtpRcptTo = new HashSet<>();
  60. private Address smtpFromAddress;
  61. private StringBuilder textBody;
  62. private StringBuilder htmlBody;
  63. private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
  64. protected Map<String, Object> soyContext;
  65. protected Map<String, Object> soyContextEmailData;
  66. protected List<String> footers;
  67. protected final EmailArguments args;
  68. protected Account.Id fromId;
  69. protected NotifyHandling notify = NotifyHandling.ALL;
  70. protected OutgoingEmail(EmailArguments ea, String mc) {
  71. args = ea;
  72. messageClass = mc;
  73. headers = new LinkedHashMap<>();
  74. }
  75. public void setFrom(Account.Id id) {
  76. fromId = id;
  77. }
  78. public void setNotify(NotifyHandling notify) {
  79. this.notify = checkNotNull(notify);
  80. }
  81. public void setAccountsToNotify(ListMultimap<RecipientType, Account.Id> accountsToNotify) {
  82. this.accountsToNotify = checkNotNull(accountsToNotify);
  83. }
  84. /**
  85. * Format and enqueue the message for delivery.
  86. *
  87. * @throws EmailException
  88. */
  89. public void send() throws EmailException {
  90. if (NotifyHandling.NONE.equals(notify) && accountsToNotify.isEmpty()) {
  91. return;
  92. }
  93. if (!args.emailSender.isEnabled()) {
  94. // Server has explicitly disabled email sending.
  95. //
  96. return;
  97. }
  98. init();
  99. if (useHtml()) {
  100. appendHtml(soyHtmlTemplate("HeaderHtml"));
  101. }
  102. format();
  103. appendText(textTemplate("Footer"));
  104. if (useHtml()) {
  105. appendHtml(soyHtmlTemplate("FooterHtml"));
  106. }
  107. Set<Address> smtpRcptToPlaintextOnly = new HashSet<>();
  108. if (shouldSendMessage()) {
  109. if (fromId != null) {
  110. Optional<AccountState> fromUser = args.accountCache.get(fromId);
  111. if (fromUser.isPresent()) {
  112. GeneralPreferencesInfo senderPrefs = fromUser.get().getGeneralPreferences();
  113. if (senderPrefs != null && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
  114. // If we are impersonating a user, make sure they receive a CC of
  115. // this message so they can always review and audit what we sent
  116. // on their behalf to others.
  117. //
  118. add(RecipientType.CC, fromId);
  119. } else if (!accountsToNotify.containsValue(fromId) && rcptTo.remove(fromId)) {
  120. // If they don't want a copy, but we queued one up anyway,
  121. // drop them from the recipient lists.
  122. //
  123. removeUser(fromUser.get().getAccount());
  124. }
  125. }
  126. }
  127. // Check the preferences of all recipients. If any user has disabled
  128. // his email notifications then drop him from recipients' list.
  129. // In addition, check if users only want to receive plaintext email.
  130. for (Account.Id id : rcptTo) {
  131. Optional<AccountState> thisUser = args.accountCache.get(id);
  132. if (thisUser.isPresent()) {
  133. Account thisUserAccount = thisUser.get().getAccount();
  134. GeneralPreferencesInfo prefs = thisUser.get().getGeneralPreferences();
  135. if (prefs == null || prefs.getEmailStrategy() == DISABLED) {
  136. removeUser(thisUserAccount);
  137. } else if (useHtml() && prefs.getEmailFormat() == EmailFormat.PLAINTEXT) {
  138. removeUser(thisUserAccount);
  139. smtpRcptToPlaintextOnly.add(
  140. new Address(thisUserAccount.getFullName(), thisUserAccount.getPreferredEmail()));
  141. }
  142. }
  143. if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) {
  144. return;
  145. }
  146. }
  147. // Set Reply-To only if it hasn't been set by a child class
  148. // Reply-To will already be populated for the message types where Gerrit supports
  149. // inbound email replies.
  150. if (!headers.containsKey(FieldName.REPLY_TO)) {
  151. StringJoiner j = new StringJoiner(", ");
  152. if (fromId != null) {
  153. Address address = toAddress(fromId);
  154. if (address != null) {
  155. j.add(address.getEmail());
  156. }
  157. }
  158. smtpRcptTo.stream().forEach(a -> j.add(a.getEmail()));
  159. smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.getEmail()));
  160. setHeader(FieldName.REPLY_TO, j.toString());
  161. }
  162. String textPart = textBody.toString();
  163. OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args();
  164. va.messageClass = messageClass;
  165. va.smtpFromAddress = smtpFromAddress;
  166. va.smtpRcptTo = smtpRcptTo;
  167. va.headers = headers;
  168. va.body = textPart;
  169. if (useHtml()) {
  170. va.htmlBody = htmlBody.toString();
  171. } else {
  172. va.htmlBody = null;
  173. }
  174. for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
  175. try {
  176. validator.validateOutgoingEmail(va);
  177. } catch (ValidationException e) {
  178. return;
  179. }
  180. }
  181. if (!smtpRcptTo.isEmpty()) {
  182. // Send multipart message
  183. args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody);
  184. }
  185. if (!smtpRcptToPlaintextOnly.isEmpty()) {
  186. // Send plaintext message
  187. Map<String, EmailHeader> shallowCopy = new HashMap<>();
  188. shallowCopy.putAll(headers);
  189. // Remove To and Cc
  190. shallowCopy.remove(FieldName.TO);
  191. shallowCopy.remove(FieldName.CC);
  192. for (Address a : smtpRcptToPlaintextOnly) {
  193. // Add new To
  194. EmailHeader.AddressList to = new EmailHeader.AddressList();
  195. to.add(a);
  196. shallowCopy.put(FieldName.TO, to);
  197. }
  198. args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body);
  199. }
  200. }
  201. }
  202. /** Format the message body by calling {@link #appendText(String)}. */
  203. protected abstract void format() throws EmailException;
  204. /**
  205. * Setup the message headers and envelope (TO, CC, BCC).
  206. *
  207. * @throws EmailException if an error occurred.
  208. */
  209. protected void init() throws EmailException {
  210. setupSoyContext();
  211. smtpFromAddress = args.fromAddressGenerator.from(fromId);
  212. setHeader(FieldName.DATE, new Date());
  213. headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
  214. headers.put(FieldName.TO, new EmailHeader.AddressList());
  215. headers.put(FieldName.CC, new EmailHeader.AddressList());
  216. setHeader(FieldName.MESSAGE_ID, "");
  217. setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
  218. for (RecipientType recipientType : accountsToNotify.keySet()) {
  219. add(recipientType, accountsToNotify.get(recipientType));
  220. }
  221. setHeader(MailHeader.MESSAGE_TYPE.fieldName(), messageClass);
  222. footers.add(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
  223. textBody = new StringBuilder();
  224. htmlBody = new StringBuilder();
  225. if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) {
  226. appendText(getFromLine());
  227. }
  228. }
  229. protected String getFromLine() {
  230. StringBuilder f = new StringBuilder();
  231. Optional<Account> account = args.accountCache.get(fromId).map(AccountState::getAccount);
  232. if (account.isPresent()) {
  233. String name = account.get().getFullName();
  234. String email = account.get().getPreferredEmail();
  235. if ((name != null && !name.isEmpty()) || (email != null && !email.isEmpty())) {
  236. f.append("From");
  237. if (name != null && !name.isEmpty()) {
  238. f.append(" ").append(name);
  239. }
  240. if (email != null && !email.isEmpty()) {
  241. f.append(" <").append(email).append(">");
  242. }
  243. f.append(":\n\n");
  244. }
  245. }
  246. return f.toString();
  247. }
  248. public String getGerritHost() {
  249. if (getGerritUrl() != null) {
  250. try {
  251. return new URL(getGerritUrl()).getHost();
  252. } catch (MalformedURLException e) {
  253. // Try something else.
  254. }
  255. }
  256. // Fall back onto whatever the local operating system thinks
  257. // this server is called. We hopefully didn't get here as a
  258. // good admin would have configured the canonical url.
  259. //
  260. return SystemReader.getInstance().getHostname();
  261. }
  262. public String getSettingsUrl() {
  263. if (getGerritUrl() != null) {
  264. final StringBuilder r = new StringBuilder();
  265. r.append(getGerritUrl());
  266. r.append("settings");
  267. return r.toString();
  268. }
  269. return null;
  270. }
  271. public String getGerritUrl() {
  272. return args.urlProvider.get();
  273. }
  274. /** Set a header in the outgoing message. */
  275. protected void setHeader(String name, String value) {
  276. headers.put(name, new EmailHeader.String(value));
  277. }
  278. /** Remove a header from the outgoing message. */
  279. protected void removeHeader(String name) {
  280. headers.remove(name);
  281. }
  282. protected void setHeader(String name, Date date) {
  283. headers.put(name, new EmailHeader.Date(date));
  284. }
  285. /** Append text to the outgoing email body. */
  286. protected void appendText(String text) {
  287. if (text != null) {
  288. textBody.append(text);
  289. }
  290. }
  291. /** Append html to the outgoing email body. */
  292. protected void appendHtml(String html) {
  293. if (html != null) {
  294. htmlBody.append(html);
  295. }
  296. }
  297. /** Lookup a human readable name for an account, usually the "full name". */
  298. protected String getNameFor(Account.Id accountId) {
  299. if (accountId == null) {
  300. return args.gerritPersonIdent.getName();
  301. }
  302. Optional<Account> account = args.accountCache.get(accountId).map(AccountState::getAccount);
  303. String name = null;
  304. if (account.isPresent()) {
  305. name = account.get().getFullName();
  306. if (name == null) {
  307. name = account.get().getPreferredEmail();
  308. }
  309. }
  310. if (name == null) {
  311. name = args.anonymousCowardName + " #" + accountId;
  312. }
  313. return name;
  314. }
  315. /**
  316. * Gets the human readable name and email for an account; if neither are available, returns the
  317. * Anonymous Coward name.
  318. *
  319. * @param accountId user to fetch.
  320. * @return name/email of account, or Anonymous Coward if unset.
  321. */
  322. public String getNameEmailFor(Account.Id accountId) {
  323. Optional<Account> account = args.accountCache.get(accountId).map(AccountState::getAccount);
  324. if (account.isPresent()) {
  325. String name = account.get().getFullName();
  326. String email = account.get().getPreferredEmail();
  327. if (name != null && email != null) {
  328. return name + " <" + email + ">";
  329. } else if (name != null) {
  330. return name;
  331. } else if (email != null) {
  332. return email;
  333. }
  334. }
  335. return args.anonymousCowardName + " #" + accountId;
  336. }
  337. /**
  338. * Gets the human readable name and email for an account; if both are unavailable, returns the
  339. * username. If no username is set, this function returns null.
  340. *
  341. * @param accountId user to fetch.
  342. * @return name/email of account, username, or null if unset.
  343. */
  344. public String getUserNameEmailFor(Account.Id accountId) {
  345. Optional<AccountState> accountState = args.accountCache.get(accountId);
  346. if (!accountState.isPresent()) {
  347. return null;
  348. }
  349. Account account = accountState.get().getAccount();
  350. String name = account.getFullName();
  351. String email = account.getPreferredEmail();
  352. if (name != null && email != null) {
  353. return name + " <" + email + ">";
  354. } else if (email != null) {
  355. return email;
  356. } else if (name != null) {
  357. return name;
  358. }
  359. return accountState.get().getUserName().orElse(null);
  360. }
  361. protected boolean shouldSendMessage() {
  362. if (textBody.length() == 0) {
  363. // If we have no message body, don't send.
  364. return false;
  365. }
  366. if (smtpRcptTo.isEmpty()) {
  367. // If we have nobody to send this message to, then all of our
  368. // selection filters previously for this type of message were
  369. // unable to match a destination. Don't bother sending it.
  370. return false;
  371. }
  372. if ((accountsToNotify == null || accountsToNotify.isEmpty())
  373. && smtpRcptTo.size() == 1
  374. && rcptTo.size() == 1
  375. && rcptTo.contains(fromId)) {
  376. // If the only recipient is also the sender, don't bother.
  377. //
  378. return false;
  379. }
  380. return true;
  381. }
  382. /** Schedule this message for delivery to the listed accounts. */
  383. protected void add(RecipientType rt, Collection<Account.Id> list) {
  384. add(rt, list, false);
  385. }
  386. /** Schedule this message for delivery to the listed accounts. */
  387. protected void add(RecipientType rt, Collection<Account.Id> list, boolean override) {
  388. for (final Account.Id id : list) {
  389. add(rt, id, override);
  390. }
  391. }
  392. /** Schedule this message for delivery to the listed address. */
  393. protected void addByEmail(RecipientType rt, Collection<Address> list) {
  394. addByEmail(rt, list, false);
  395. }
  396. /** Schedule this message for delivery to the listed address. */
  397. protected void addByEmail(RecipientType rt, Collection<Address> list, boolean override) {
  398. for (final Address id : list) {
  399. add(rt, id, override);
  400. }
  401. }
  402. protected void add(RecipientType rt, UserIdentity who) {
  403. add(rt, who, false);
  404. }
  405. protected void add(RecipientType rt, UserIdentity who, boolean override) {
  406. if (who != null && who.getAccount() != null) {
  407. add(rt, who.getAccount(), override);
  408. }
  409. }
  410. /** Schedule delivery of this message to the given account. */
  411. protected void add(RecipientType rt, Account.Id to) {
  412. add(rt, to, false);
  413. }
  414. protected void add(RecipientType rt, Account.Id to, boolean override) {
  415. try {
  416. if (!rcptTo.contains(to) && isVisibleTo(to)) {
  417. rcptTo.add(to);
  418. add(rt, toAddress(to), override);
  419. }
  420. } catch (PermissionBackendException e) {
  421. log.error("Error reading database for account: " + to, e);
  422. }
  423. }
  424. /**
  425. * @param to account.
  426. * @throws PermissionBackendException
  427. * @return whether this email is visible to the given account.
  428. */
  429. protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
  430. return true;
  431. }
  432. /** Schedule delivery of this message to the given account. */
  433. protected void add(RecipientType rt, Address addr) {
  434. add(rt, addr, false);
  435. }
  436. protected void add(RecipientType rt, Address addr, boolean override) {
  437. if (addr != null && addr.getEmail() != null && addr.getEmail().length() > 0) {
  438. if (!args.validator.isValid(addr.getEmail())) {
  439. log.warn("Not emailing " + addr.getEmail() + " (invalid email address)");
  440. } else if (!args.emailSender.canEmail(addr.getEmail())) {
  441. log.warn("Not emailing " + addr.getEmail() + " (prohibited by allowrcpt)");
  442. } else {
  443. if (!smtpRcptTo.add(addr)) {
  444. if (!override) {
  445. return;
  446. }
  447. ((EmailHeader.AddressList) headers.get(FieldName.TO)).remove(addr.getEmail());
  448. ((EmailHeader.AddressList) headers.get(FieldName.CC)).remove(addr.getEmail());
  449. }
  450. switch (rt) {
  451. case TO:
  452. ((EmailHeader.AddressList) headers.get(FieldName.TO)).add(addr);
  453. break;
  454. case CC:
  455. ((EmailHeader.AddressList) headers.get(FieldName.CC)).add(addr);
  456. break;
  457. case BCC:
  458. break;
  459. }
  460. }
  461. }
  462. }
  463. private Address toAddress(Account.Id id) {
  464. Optional<Account> accountState = args.accountCache.get(id).map(AccountState::getAccount);
  465. if (!accountState.isPresent()) {
  466. return null;
  467. }
  468. Account account = accountState.get();
  469. String e = account.getPreferredEmail();
  470. if (!account.isActive() || e == null) {
  471. return null;
  472. }
  473. return new Address(account.getFullName(), e);
  474. }
  475. protected void setupSoyContext() {
  476. soyContext = new HashMap<>();
  477. footers = new ArrayList<>();
  478. soyContext.put("messageClass", messageClass);
  479. soyContext.put("footers", footers);
  480. soyContextEmailData = new HashMap<>();
  481. soyContextEmailData.put("settingsUrl", getSettingsUrl());
  482. soyContextEmailData.put("instanceName", getInstanceName());
  483. soyContextEmailData.put("gerritHost", getGerritHost());
  484. soyContextEmailData.put("gerritUrl", getGerritUrl());
  485. soyContext.put("email", soyContextEmailData);
  486. }
  487. private String getInstanceName() {
  488. return args.instanceNameProvider.get();
  489. }
  490. private String soyTemplate(String name, SanitizedContent.ContentKind kind) {
  491. return args.soyTofu
  492. .newRenderer("com.google.gerrit.server.mail.template." + name)
  493. .setContentKind(kind)
  494. .setData(soyContext)
  495. .render();
  496. }
  497. protected String textTemplate(String name) {
  498. return soyTemplate(name, SanitizedContent.ContentKind.TEXT);
  499. }
  500. protected String soyHtmlTemplate(String name) {
  501. return soyTemplate(name, SanitizedContent.ContentKind.HTML);
  502. }
  503. public String joinStrings(Iterable<Object> in, String joiner) {
  504. return joinStrings(in.iterator(), joiner);
  505. }
  506. public String joinStrings(Iterator<Object> in, String joiner) {
  507. if (!in.hasNext()) {
  508. return "";
  509. }
  510. Object first = in.next();
  511. if (!in.hasNext()) {
  512. return safeToString(first);
  513. }
  514. StringBuilder r = new StringBuilder();
  515. r.append(safeToString(first));
  516. while (in.hasNext()) {
  517. r.append(joiner).append(safeToString(in.next()));
  518. }
  519. return r.toString();
  520. }
  521. protected void removeUser(Account user) {
  522. String fromEmail = user.getPreferredEmail();
  523. for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext(); ) {
  524. if (j.next().getEmail().equals(fromEmail)) {
  525. j.remove();
  526. }
  527. }
  528. for (Map.Entry<String, EmailHeader> entry : headers.entrySet()) {
  529. // Don't remove fromEmail from the "From" header though!
  530. if (entry.getValue() instanceof AddressList && !entry.getKey().equals("From")) {
  531. ((AddressList) entry.getValue()).remove(fromEmail);
  532. }
  533. }
  534. }
  535. private static String safeToString(Object obj) {
  536. return obj != null ? obj.toString() : "";
  537. }
  538. protected final boolean useHtml() {
  539. return args.settings.html && supportsHtml();
  540. }
  541. /** Override this method to enable HTML in a subclass. */
  542. protected boolean supportsHtml() {
  543. return false;
  544. }
  545. }