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 : }
|