package fr.openwide.core.spring.notification.service; import static com.google.common.base.Preconditions.checkNotNull; import static fr.openwide.core.spring.property.SpringPropertyIds.DEFAULT_LOCALE; import static fr.openwide.core.spring.property.SpringPropertyIds.NOTIFICATION_MAIL_DISABLED_RECIPIENT_FALLBACK; import static fr.openwide.core.spring.property.SpringPropertyIds.NOTIFICATION_MAIL_FROM; import static fr.openwide.core.spring.property.SpringPropertyIds.NOTIFICATION_MAIL_RECIPIENTS_FILTERED; import static fr.openwide.core.spring.property.SpringPropertyIds.NOTIFICATION_MAIL_SUBJECT_PREFIX; import static fr.openwide.core.spring.property.SpringPropertyIds.NOTIFICATION_TEST_EMAILS; import java.io.File; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.mail.Address; import javax.mail.Message.RecipientType; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import org.apache.commons.lang3.StringEscapeUtils; import org.javatuples.LabelValue; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Collections2; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import fr.openwide.core.jpa.exception.ServiceException; import fr.openwide.core.spring.notification.exception.NotificationContentRenderingException; import fr.openwide.core.spring.notification.model.INotificationContentDescriptor; import fr.openwide.core.spring.notification.model.INotificationRecipient; import fr.openwide.core.spring.notification.model.NotificationTarget; import fr.openwide.core.spring.notification.model.SimpleRecipient; import fr.openwide.core.spring.notification.service.impl.ExplicitelyDefinedNotificationContentDescriptorImpl; import fr.openwide.core.spring.notification.service.impl.FirstNotNullNotificationContentDescriptorImpl; import fr.openwide.core.spring.notification.service.impl.FreemarkerTemplateNotificationContentDescriptorImpl; import fr.openwide.core.spring.notification.util.NotificationUtils; import fr.openwide.core.spring.util.SpringBeanUtils; import fr.openwide.core.spring.util.StringUtils; import freemarker.template.Configuration; public class NotificationBuilder implements INotificationBuilderInitState, INotificationBuilderBaseState, INotificationBuilderBuildState, INotificationBuilderBodyState, INotificationBuilderContentState, INotificationBuilderTemplateState, INotificationBuilderSendState { private static final Charset DEFAULT_MAIL_CHARSET = Charsets.UTF_8; private static final String NEW_LINE_TEXT_PLAIN = StringUtils.NEW_LINE_ANTISLASH_N; private static final String NEW_LINE_HTML = "<br />"; private static final String DEV_SUBJECT_PREFIX = "[Dev]"; private static final Function<String, NotificationTarget> ADDRESS_TO_TARGET_FUNCTION = new Function<String, NotificationTarget>() { @Override public NotificationTarget apply(String address) { if (address == null) { return null; } return NotificationTarget.of(address); } }; private static final Function<INotificationRecipient, NotificationTarget> I_NOTIFICATION_RECIPIENT_TO_TARGET_FUNCTION = new Function<INotificationRecipient, NotificationTarget>() { @Override public NotificationTarget apply(INotificationRecipient recipient) { if (recipient == null) { return null; } return NotificationTarget.of(recipient.getEmail()); } }; @Autowired private JavaMailSender mailSender; @Autowired private fr.openwide.core.spring.property.service.IPropertyService propertyService; @Autowired @Qualifier(value = "freemarkerMailConfiguration") private Configuration templateConfiguration; private ApplicationContext applicationContext; private String from; private NotificationTarget replyTo; private final Map<NotificationTarget, INotificationRecipient> toByAddress = Maps.newLinkedHashMap(); private final Map<NotificationTarget, INotificationRecipient> ccByAddress = Maps.newLinkedHashMap(); private final Map<NotificationTarget, INotificationRecipient> bccByAddress = Maps.newLinkedHashMap(); private final Set<NotificationTarget> recipientsToIgnore = Sets.newHashSet(); private String subjectPrefix; private INotificationContentDescriptor userContentDescriptor; private FreemarkerTemplateNotificationContentDescriptorImpl templateContentDescriptor; private ExplicitelyDefinedNotificationContentDescriptorImpl explicitelyDefinedContentDescriptor; /** * This is not a map, since duplicate filenames are allowed. This is not a multimap either, since mutliple * attachements with the same name are not related to each other. */ private final Collection<LabelValue<String, File>> attachments = Lists.newArrayList(); private final Map<String, File> inlines = Maps.newHashMap(); private final Multimap<String, String> headers = LinkedHashMultimap.create(); private int priority; private Charset charset; protected NotificationBuilder() { this(DEFAULT_MAIL_CHARSET); } protected NotificationBuilder(Charset charset) { this.charset = charset; } public static INotificationBuilderInitState create() { return new NotificationBuilder(); } public static INotificationBuilderInitState create(Charset charset) { return new NotificationBuilder(charset); } @Override public INotificationBuilderBaseState init(ApplicationContext applicationContext) { SpringBeanUtils.autowireBean(applicationContext, this); this.applicationContext = applicationContext; this.subjectPrefix = propertyService.get(NOTIFICATION_MAIL_SUBJECT_PREFIX); this.explicitelyDefinedContentDescriptor = new ExplicitelyDefinedNotificationContentDescriptorImpl(getDefaultLocale()); SpringBeanUtils.autowireBean(applicationContext, explicitelyDefinedContentDescriptor); return this; } @Override public INotificationBuilderReplyToState from(String from) { Assert.hasText(from, "Sender's email address must contain text"); this.from = from; return this; } @Override public INotificationBuilderToState replyToAddress(String replyTo) { if (StringUtils.hasText(replyTo)) { this.replyTo = NotificationTarget.of(replyTo); } else { this.replyTo = null; } return this; } @Override public INotificationBuilderToState replyTo(INotificationRecipient replyTo) { if (replyTo != null) { this.replyTo = NotificationTarget.of(replyTo, charset); } else { this.replyTo = null; } return this; } @Override @Deprecated public INotificationBuilderBuildState to(String... to) { if (!ObjectUtils.isEmpty(to)) { toAddress(Lists.newArrayList(to)); } return this; } @Override public INotificationBuilderBuildState toAddress(String toFirst, String... toOthers) { return toAddress(Lists.asList(toFirst, toOthers)); } @Override public INotificationBuilderBuildState toAddress(Collection<String> to) { if (to != null) { for (String email : to) { if (StringUtils.hasText(email)) { addRecipient(toByAddress, new SimpleRecipient(getDefaultLocale(), email, null)); } } } return this; } @Override public INotificationBuilderBuildState to(INotificationRecipient toFirst, INotificationRecipient ... toOthers) { return to(Lists.asList(toFirst, toOthers)); } @Override public INotificationBuilderBuildState to(Collection<? extends INotificationRecipient> to) { addRecipients(toByAddress, to); return this; } @Override @Deprecated public INotificationBuilderBuildState cc(String... cc) { if (!ObjectUtils.isEmpty(cc)) { ccAddress(Lists.newArrayList(cc)); } return this; } @Override public INotificationBuilderBuildState ccAddress(String ccFirst, String... ccOthers) { return ccAddress(Lists.asList(ccFirst, ccOthers)); } @Override public INotificationBuilderBuildState ccAddress(Collection<String> cc) { addAddresses(ccByAddress, cc); return this; } @Override public INotificationBuilderBuildState cc(INotificationRecipient ccFirst, INotificationRecipient ... ccOthers) { return cc(Lists.asList(ccFirst, ccOthers)); } @Override public INotificationBuilderBuildState cc(Collection<? extends INotificationRecipient> cc) { addRecipients(ccByAddress, cc); return this; } @Override @Deprecated public INotificationBuilderBuildState bcc(String... bcc) { if (!ObjectUtils.isEmpty(bcc)) { bccAddress(Lists.newArrayList(bcc)); } return this; } @Override public INotificationBuilderBuildState bccAddress(String bccFirst, String... bccOthers) { return bccAddress(Lists.asList(bccFirst, bccOthers)); } @Override public INotificationBuilderBuildState bccAddress(Collection<String> bcc) { addAddresses(bccByAddress, bcc); return this; } @Override public INotificationBuilderBuildState bcc(INotificationRecipient bccFirst, INotificationRecipient ... bccOthers) { return bcc(Lists.asList(bccFirst, bccOthers)); } @Override public INotificationBuilderBuildState bcc(Collection<? extends INotificationRecipient> bcc) { addRecipients(bccByAddress, bcc); return this; } @Override public INotificationBuilderBuildState exceptAddress(Collection<String> except) { recipientsToIgnore.addAll(Collections2.transform(except, ADDRESS_TO_TARGET_FUNCTION)); return this; } @Override public INotificationBuilderBuildState exceptAddress(String exceptFirst, String... exceptOthers) { return exceptAddress(Lists.asList(exceptFirst, exceptOthers)); } @Override public INotificationBuilderBuildState except(INotificationRecipient exceptFirst, INotificationRecipient... exceptOthers) { return except(Lists.asList(exceptFirst, exceptOthers)); } @Override public INotificationBuilderBuildState except(Collection<? extends INotificationRecipient> except) { recipientsToIgnore.addAll(Collections2.transform(except, I_NOTIFICATION_RECIPIENT_TO_TARGET_FUNCTION)); return this; } @Override public INotificationBuilderTemplateState template(String templateKey) { Assert.hasText(templateKey, "Template key must contain text"); this.templateContentDescriptor = new FreemarkerTemplateNotificationContentDescriptorImpl( templateConfiguration, templateKey, Collections.unmodifiableCollection(attachments), getDefaultLocale() ); SpringBeanUtils.autowireBean(applicationContext, this.templateContentDescriptor); return this; } @Override public INotificationBuilderTemplateState variable(String name, Object value) { return variable(name, value, null); } /** * If locale == null, the variable will be considered as not locale-sensitive and will be available for all locales */ @Override public INotificationBuilderTemplateState variable(String name, Object value, Locale locale) { Assert.hasText(name, "Variable name must contain text"); templateContentDescriptor.setVariable(locale, name, value); return this; } @Override public INotificationBuilderTemplateState variables(Map<String, ?> variables) { return variables(variables, null); } @Override public INotificationBuilderTemplateState variables(Map<String, ?> variables, Locale locale) { templateContentDescriptor.setVariables(locale, variables); return this; } @Override public INotificationBuilderContentState content(INotificationContentDescriptor contentDescriptor) { this.userContentDescriptor = checkNotNull(contentDescriptor); return this; } @Override public INotificationBuilderBodyState subject(String subject) { Assert.hasText(subject, "Email subject must contain text"); this.explicitelyDefinedContentDescriptor.setSubject(null, subject); return this; } @Override public INotificationBuilderBuildState subjectPrefix(String prefix) { this.subjectPrefix = prefix; return this; } @Override public INotificationBuilderBodyState subject(String prefix, String subject) { subjectPrefix(prefix); subject(subject); return this; } @Override public INotificationBuilderSendState textBody(String textBody) { return textBody(textBody, null); } @Override public INotificationBuilderSendState textBody(String textBody, Locale locale) { this.explicitelyDefinedContentDescriptor.setTextBody(locale, textBody); return this; } @Override public INotificationBuilderSendState htmlBody(String htmlBody) { return htmlBody(htmlBody, null); } // If locale == null, the associated htmlBody will be used for any locale that do not have one @Override public INotificationBuilderSendState htmlBody(String htmlBody, Locale locale) { this.explicitelyDefinedContentDescriptor.setHtmlBody(locale, htmlBody); return this; } @Override public INotificationBuilderSendState attach(String attachmentFilename, File file) { Assert.hasText(attachmentFilename, "Attachment filename must contain text"); Assert.notNull(file, "Attached file must not be null"); this.attachments.add(LabelValue.with(attachmentFilename, file)); return this; } @Override public INotificationBuilderSendState attach(Collection<LabelValue<String, File>> attachments) { Assert.notNull(attachments, "Attachment map must not be null"); this.attachments.addAll(attachments); return this; } @Override public INotificationBuilderSendState attach(Map<String, File> attachments) { Assert.notNull(attachments, "Attachment map must not be null"); for (Map.Entry<String, File> attachment : attachments.entrySet()) { attach(attachment.getKey(), attachment.getValue()); } return this; } @Override public INotificationBuilderSendState inline(String contentId, File file) { Assert.hasText(contentId, "Content ID must contain text"); Assert.notNull(file, "Inline file must not be null"); this.inlines.put(contentId, file); return this; } @Override public INotificationBuilderSendState header(String name, String value) { Assert.hasText(name, "Header name must contain text"); Assert.hasText(value, "Header value must contain text"); this.headers.put(name, value); return this; } @Override public INotificationBuilderSendState headers(MultiValueMap<String, String> headers) { Assert.notNull(headers, "Header map must not be null"); for (Entry<String, List<String>> header : headers.entrySet()) { for (String value : header.getValue()) { header(header.getKey(), value); } } return this; } @Override public INotificationBuilderSendState priority(int priority) { if (priority < 1 || priority > 5) { throw new IllegalArgumentException("Priority must be between 1 (highest) and 5 (lowest)"); } this.priority = priority; return this; } @Override public void send() throws ServiceException { if (!NotificationUtils.isNotificationsEnabled()) { return; } removeDuplicatesAndRecipientsToIgnoreBeforeSendingMessage(); for (Entry<INotificationContentDescriptor, Targets> entry : groupTargetsByContext()) { INotificationContentDescriptor contentDescriptor = entry.getKey(); Targets targets = entry.getValue(); if (targets.to.isEmpty() && targets.cc.isEmpty() && targets.bcc.isEmpty()) { // Multimap.get(unknown key) returns an empty collection continue; } MimeMessage message; Address[] effectiveTo; Address[] effectiveCc; Address[] effectiveBcc; Address[] from; Address[] replyTo; try { // Step 1 : Build message. Effective recipients could be different than expected ones, we store. message = buildMessage(contentDescriptor, targets, charset.name()); effectiveTo = message.getRecipients(RecipientType.TO); effectiveCc = message.getRecipients(RecipientType.CC); effectiveBcc = message.getRecipients(RecipientType.BCC); replyTo = message.getReplyTo(); from = message.getFrom(); } catch (NotificationContentRenderingException e) { throw new ServiceException("Error while rendering email notification", e); } catch (MessagingException e) { throw new ServiceException( String.format("Error building the MIME message (to:%s, cc:%s, bcc:%s)", targets.to, targets.cc, targets.bcc), e ); } try { // Step 2 : Send message. mailSender.send(message); } catch (MailException e) { throw new ServiceException( String.format("Error sending email notification " + "(from:%s, reply-to: %s; " + "original to:%s, cc:%s, bcc:%s; " + "effective to:%s, cc:%s, bcc:%s)", Arrays.toString(from), Arrays.toString(replyTo), targets.to, targets.cc, targets.bcc, Arrays.toString(effectiveTo), Arrays.toString(effectiveCc), Arrays.toString(effectiveBcc)), e ); } } } private static class Targets { private final Collection<NotificationTarget> to = Lists.newArrayList(); private final Collection<NotificationTarget> cc = Lists.newArrayList(); private final Collection<NotificationTarget> bcc = Lists.newArrayList(); public void to(NotificationTarget target) { to.add(target); } public void cc(NotificationTarget target) { cc.add(target); } public void bcc(NotificationTarget target) { bcc.add(target); } } private static class TargetContextGrouper { private final INotificationContentDescriptor noContextContentDescriptor; private final Map<INotificationContentDescriptor, Targets> data = Maps.newLinkedHashMap(); public TargetContextGrouper(INotificationContentDescriptor noContextContentDescriptor) { super(); this.noContextContentDescriptor = noContextContentDescriptor; } public void to(NotificationTarget target, INotificationRecipient context) { get(context).to(target); } public void cc(NotificationTarget target, INotificationRecipient context) { get(context).cc(target); } public void bcc(NotificationTarget target, INotificationRecipient context) { get(context).bcc(target); } private Targets get(INotificationRecipient context) { INotificationContentDescriptor contentDescriptorWithContext = noContextContentDescriptor.withContext(context); Targets result = data.get(contentDescriptorWithContext); if (result == null) { result = new Targets(); data.put(contentDescriptorWithContext, result); } return result; } public Iterable<Map.Entry<INotificationContentDescriptor, Targets>> getAll() { return data.entrySet(); } } private Iterable<Entry<INotificationContentDescriptor, Targets>> groupTargetsByContext() { INotificationContentDescriptor contentDescriptor = chooseNotificationContentDescriptor(); TargetContextGrouper result = new TargetContextGrouper(contentDescriptor); for (Map.Entry<NotificationTarget, INotificationRecipient> entry : toByAddress.entrySet()) { result.to(entry.getKey(), entry.getValue()); } for (Map.Entry<NotificationTarget, INotificationRecipient> entry : ccByAddress.entrySet()) { result.cc(entry.getKey(), entry.getValue()); } for (Map.Entry<NotificationTarget, INotificationRecipient> entry : bccByAddress.entrySet()) { result.bcc(entry.getKey(), entry.getValue()); } return result.getAll(); } private void addAddresses(Map<NotificationTarget, INotificationRecipient> recipientsByTarget, Iterable<? extends String> addresses) { if (addresses != null) { for (String address : addresses) { addAddress(recipientsByTarget, address); } } } private void addRecipients(Map<NotificationTarget, INotificationRecipient> recipientsByTarget, Iterable<? extends INotificationRecipient> recipients) { if (recipients != null) { for (INotificationRecipient recipient : recipients) { addRecipient(recipientsByTarget, recipient); } } } private void addAddress(Map<NotificationTarget, INotificationRecipient> recipientsByTarget, String address) { if (StringUtils.hasText(address)) { addRecipient(recipientsByTarget, new SimpleRecipient(getDefaultLocale(), address, null)); } } private void addRecipient(Map<NotificationTarget, INotificationRecipient> recipientsByTarget, INotificationRecipient recipient) { if (recipient.isNotificationEnabled() && StringUtils.hasText(recipient.getEmail())) { addRecipientUnsafe( recipientsByTarget, NotificationTarget.of(recipient, charset), recipient ); } else if (propertyService.get(NOTIFICATION_MAIL_DISABLED_RECIPIENT_FALLBACK) != null) { for (String redirectionEmail : propertyService.get(NOTIFICATION_MAIL_DISABLED_RECIPIENT_FALLBACK) ) { addRecipientUnsafe( recipientsByTarget, NotificationTarget.of(redirectionEmail), new SimpleRecipient(recipient, redirectionEmail) ); } } } private void addRecipientUnsafe(Map<NotificationTarget, INotificationRecipient> recipientsByTarget, NotificationTarget target, INotificationRecipient recipient) { if (!recipientsByTarget.containsKey(target)) { recipientsByTarget.put(target, recipient); } } private void removeDuplicatesAndRecipientsToIgnoreBeforeSendingMessage() { toByAddress.keySet().removeAll(recipientsToIgnore); ccByAddress.keySet().removeAll(recipientsToIgnore); ccByAddress.keySet().removeAll(toByAddress.keySet()); bccByAddress.keySet().removeAll(recipientsToIgnore); bccByAddress.keySet().removeAll(toByAddress.keySet()); bccByAddress.keySet().removeAll(ccByAddress.keySet()); } private INotificationContentDescriptor chooseNotificationContentDescriptor() { List<INotificationContentDescriptor> descriptors = Lists.newArrayList(); if (userContentDescriptor != null) { // Maximum priority descriptors.add(userContentDescriptor); } if (templateContentDescriptor != null) { descriptors.add(templateContentDescriptor); } descriptors.add(explicitelyDefinedContentDescriptor); // Minimum priority return new FirstNotNullNotificationContentDescriptorImpl(descriptors); } private MimeMessage buildMessage(INotificationContentDescriptor contentDescriptor, Targets recipients, String encoding) throws NotificationContentRenderingException, MessagingException { String subject = buildSubject(contentDescriptor.renderSubject()); String textBody = contentDescriptor.renderTextBody(); String htmlBody = contentDescriptor.renderHtmlBody(); MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, isMultipartNeeded(textBody, htmlBody), encoding); if (from == null) { from = getDefaultFrom(); } Collection<NotificationTarget> filteredTos = filterTo(recipients.to); Collection<NotificationTarget> filteredCcs = filterCcBcc(recipients.cc); Collection<NotificationTarget> filteredBccs = filterCcBcc(recipients.bcc); if (filteredTos.isEmpty() && filteredCcs.isEmpty() && filteredBccs.isEmpty()) { return null; } helper.setFrom(from); if (replyTo != null) { helper.setReplyTo(replyTo.getAddress()); } for (NotificationTarget to : filteredTos) { helper.addTo(to.getAddress()); } for (NotificationTarget cc : filteredCcs) { helper.addCc(cc.getAddress()); } for (NotificationTarget bcc : filteredBccs) { helper.addBcc(bcc.getAddress()); } helper.setSubject(subject); String textBodyPrefix = getBodyPrefix(recipients.to, recipients.cc, recipients.bcc, encoding, MailFormat.TEXT); String htmlBodyPrefix = getBodyPrefix(recipients.to, recipients.cc, recipients.bcc, encoding, MailFormat.HTML); if (StringUtils.hasText(textBody) && StringUtils.hasText(htmlBody)) { helper.setText(textBodyPrefix + textBody, htmlBodyPrefix + htmlBody); } else if (StringUtils.hasText(htmlBody)) { helper.setText(htmlBodyPrefix + htmlBody, true); } else { helper.setText(textBodyPrefix + textBody); } for (LabelValue<String, File> attachment : attachments) { helper.addAttachment(attachment.getLabel(), attachment.getValue()); } for (Map.Entry<String, File> inline : inlines.entrySet()) { helper.addInline(inline.getKey(), inline.getValue()); } for (Entry<String, Collection<String>> header : headers.asMap().entrySet()) { for (String value : header.getValue()) { message.addHeader(header.getKey(), value); } } if (priority != 0) { helper.setPriority(priority); } return message; } private String buildSubject(String subject) { StringBuilder builder = new StringBuilder(); if (StringUtils.hasText(subjectPrefix)) { builder.append(subjectPrefix); } if (propertyService.isConfigurationTypeDevelopment()) { builder.append(DEV_SUBJECT_PREFIX); } if (builder.length() > 0) { builder.append(" "); } builder.append(subject); return builder.toString(); } private boolean isMultipartNeeded(String textBody, String htmlBody) { boolean multipleBodies = StringUtils.hasText(textBody) && StringUtils.hasText(htmlBody); return multipleBodies || !attachments.isEmpty() || !inlines.isEmpty(); } private String getDefaultFrom() { return propertyService.get(NOTIFICATION_MAIL_FROM); } private Locale getDefaultLocale() { return propertyService.get(DEFAULT_LOCALE); } protected boolean isMailRecipientsFiltered() { return propertyService.isConfigurationTypeDevelopment() || propertyService.get(NOTIFICATION_MAIL_RECIPIENTS_FILTERED); } private Collection<NotificationTarget> filterTo(Collection<NotificationTarget> emails) { if (isMailRecipientsFiltered()) { return getNotificationTestEmails(); } return emails; } protected Collection<NotificationTarget> getNotificationTestEmails() { return Collections2.transform(propertyService.get(NOTIFICATION_TEST_EMAILS), ADDRESS_TO_TARGET_FUNCTION); } private Collection<NotificationTarget> filterCcBcc(Collection<NotificationTarget> emails) { if (isMailRecipientsFiltered()) { return Sets.newHashSetWithExpectedSize(0); } return emails; } private String getBodyPrefix(Collection<NotificationTarget> to, Collection<NotificationTarget> cc, Collection<NotificationTarget> bcc, String encoding, MailFormat mailFormat) { if (isMailRecipientsFiltered()) { String newLine = MailFormat.HTML.equals(mailFormat) ? NEW_LINE_HTML : NEW_LINE_TEXT_PLAIN; StringBuffer newBody = new StringBuffer(); newBody.append("#############").append(newLine); newBody.append("#").append(newLine); newBody.append("# To: ").append(renderAddressesForDebug(to, mailFormat)).append(newLine); newBody.append("# Cc: ").append(renderAddressesForDebug(cc, mailFormat)).append(newLine); newBody.append("# Bcc: ").append(renderAddressesForDebug(bcc, mailFormat)).append(newLine); newBody.append("#").append(newLine); newBody.append("# Encoding: ").append(encoding).append(newLine); newBody.append("#").append(newLine); newBody.append("#############").append(newLine).append(newLine).append(newLine); return newBody.toString(); } return ""; } private String renderAddressesForDebug(Collection<NotificationTarget> addresses, MailFormat mailFormat) { String debug; if (addresses != null && !addresses.isEmpty()) { debug = Joiner.on(", ").join(addresses); } else { debug = "<none>"; } if (MailFormat.HTML.equals(mailFormat)) { return StringEscapeUtils.escapeHtml4(debug); } else { return debug; } } private enum MailFormat { HTML, TEXT } }