org.dbflute.mail.send.embedded.postie.SMailHonestPostie Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mailflute Show documentation
Show all versions of mailflute Show documentation
simple mail library with DBFlute and Java8
/*
* Copyright 2014-2015 the original author or authors.
*
* 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 org.dbflute.mail.send.embedded.postie;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;
import javax.mail.internet.MimeUtility;
import javax.mail.util.ByteArrayDataSource;
import org.dbflute.helper.message.ExceptionMessageBuilder;
import org.dbflute.mail.Postcard;
import org.dbflute.mail.send.SMailPostalMotorbike;
import org.dbflute.mail.send.SMailPostie;
import org.dbflute.mail.send.exception.SMailIllegalStateException;
import org.dbflute.mail.send.exception.SMailMessageSettingFailureException;
import org.dbflute.mail.send.exception.SMailTransportFailureException;
import org.dbflute.mail.send.supplement.async.SMailAsyncStrategy;
import org.dbflute.mail.send.supplement.async.SMailAsyncStrategyNone;
import org.dbflute.mail.send.supplement.attachment.SMailAttachment;
import org.dbflute.mail.send.supplement.filter.SMailAddressFilter;
import org.dbflute.mail.send.supplement.filter.SMailAddressFilterNone;
import org.dbflute.mail.send.supplement.filter.SMailBodyTextFilter;
import org.dbflute.mail.send.supplement.filter.SMailBodyTextFilterNone;
import org.dbflute.mail.send.supplement.filter.SMailCancelFilter;
import org.dbflute.mail.send.supplement.filter.SMailCancelFilterNone;
import org.dbflute.mail.send.supplement.filter.SMailSubjectFilter;
import org.dbflute.mail.send.supplement.filter.SMailSubjectFilterNone;
import org.dbflute.mail.send.supplement.label.SMailLabelStrategy;
import org.dbflute.mail.send.supplement.label.SMailLabelStrategyNone;
import org.dbflute.mail.send.supplement.logging.SMailLoggingStrategy;
import org.dbflute.mail.send.supplement.logging.SMailTypicalLoggingStrategy;
import org.dbflute.optional.OptionalThing;
/**
* @author jflute
* @since 0.4.0 (2015/05/05 Tuesday)
*/
public class SMailHonestPostie implements SMailPostie {
// ===================================================================================
// Definition
// ==========
private static final SMailCancelFilter noneCancelFilter = new SMailCancelFilterNone();
private static final SMailAddressFilter noneAddressFilter = new SMailAddressFilterNone();
private static final SMailSubjectFilter noneSubjectFilter = new SMailSubjectFilterNone();
private static final SMailBodyTextFilter noneBodyTextFilter = new SMailBodyTextFilterNone();
private static final SMailAsyncStrategy noneAsyncStrategy = new SMailAsyncStrategyNone();
private static final SMailLabelStrategy noneLabelStrategy = new SMailLabelStrategyNone();
private static final SMailLoggingStrategy typicalLoggingStrategy = new SMailTypicalLoggingStrategy();
// ===================================================================================
// Attribute
// =========
protected final SMailPostalMotorbike motorbike; // not null
protected SMailCancelFilter cancelFilter = noneCancelFilter; // not null
protected SMailAddressFilter addressFilter = noneAddressFilter; // not null
protected SMailSubjectFilter subjectFilter = noneSubjectFilter; // not null
protected SMailBodyTextFilter bodyTextFilter = noneBodyTextFilter; // not null
protected SMailAsyncStrategy asyncStrategy = noneAsyncStrategy; // not null
protected SMailLabelStrategy labelStrategy = noneLabelStrategy; // not null
protected SMailLoggingStrategy loggingStrategy = typicalLoggingStrategy; // not null
protected boolean training;
// ===================================================================================
// Constructor
// ===========
public SMailHonestPostie(SMailPostalMotorbike motorbike) {
assertArgumentNotNull("motorbike", motorbike);
this.motorbike = motorbike;
}
public SMailHonestPostie withCancelFilter(SMailCancelFilter cancelFilter) {
assertArgumentNotNull("cancelFilter", cancelFilter);
this.cancelFilter = cancelFilter;
return this;
}
public SMailHonestPostie withAddressFilter(SMailAddressFilter addressFilter) {
assertArgumentNotNull("addressFilter", addressFilter);
this.addressFilter = addressFilter;
return this;
}
public SMailHonestPostie withSubjectFilter(SMailSubjectFilter subjectFilter) {
assertArgumentNotNull("subjectFilter", subjectFilter);
this.subjectFilter = subjectFilter;
return this;
}
public SMailHonestPostie withBodyTextFilter(SMailBodyTextFilter bodyTextFilter) {
assertArgumentNotNull("bodyTextFilter", bodyTextFilter);
this.bodyTextFilter = bodyTextFilter;
return this;
}
public SMailHonestPostie withAsyncStrategy(SMailAsyncStrategy asyncStrategy) {
assertArgumentNotNull("asyncStrategy", asyncStrategy);
this.asyncStrategy = asyncStrategy;
return this;
}
public SMailHonestPostie withLabelStrategy(SMailLabelStrategy labelStrategy) {
assertArgumentNotNull("labelStrategy", labelStrategy);
this.labelStrategy = labelStrategy;
return this;
}
public SMailHonestPostie withLoggingStrategy(SMailLoggingStrategy loggingStrategy) {
assertArgumentNotNull("loggingStrategy", loggingStrategy);
this.loggingStrategy = loggingStrategy;
return this;
}
public SMailHonestPostie asTraining() {
training = true;
return this;
}
// ===================================================================================
// Deliver
// =======
@Override
public void deliver(Postcard postcard) {
final SMailPostingMessage message = createMailMessage(postcard);
if (isCancel(postcard)) {
return; // no logging here, only filter knows the reason
}
prepareAddress(postcard, message);
prepareSubject(postcard, message);
prepareBody(postcard, message);
send(postcard, message);
}
protected SMailPostingMessage createMailMessage(Postcard postcard) {
final MimeMessage mimeMessage = createMimeMessage(extractNativeSession(motorbike));
final Map pushedLoggingMap = postcard.getPushedLoggingMap();
final Map> officeManagedLoggingMap = postcard.getOfficeManagedLoggingMap();
return new SMailPostingMessage(mimeMessage, motorbike, training, pushedLoggingMap, officeManagedLoggingMap);
}
protected Session extractNativeSession(SMailPostalMotorbike motorbike) {
return motorbike.getNativeSession();
}
protected MimeMessage createMimeMessage(Session session) {
return new MimeMessage(session);
}
protected boolean isCancel(Postcard postcard) {
return cancelFilter.isCancel(postcard);
}
// ===================================================================================
// Prepare Address
// ===============
protected void prepareAddress(Postcard postcard, SMailPostingMessage message) {
final Address fromAddress = postcard.getFrom().orElseThrow(() -> { /* already checked, but just in case */
return new SMailIllegalStateException("Not found the from address in the postcard: " + postcard);
});
resolveAddressPersonalLabel(postcard, fromAddress);
final Address filteredFrom = addressFilter.filterFrom(postcard, fromAddress);
message.setFrom(verifyFilteredFromAddress(postcard, filteredFrom));
boolean existsToAddress = false;
for (Address to : postcard.getToList()) {
resolveAddressPersonalLabel(postcard, to);
final OptionalThing opt = addressFilter.filterTo(postcard, to);
verifyFilteredOptionalAddress(postcard, opt).ifPresent(address -> message.addTo(address));
if (opt.isPresent()) {
existsToAddress = true;
}
}
verifyFilteredToAddressExists(postcard, existsToAddress);
for (Address cc : postcard.getCcList()) {
resolveAddressPersonalLabel(postcard, cc);
final OptionalThing opt = addressFilter.filterCc(postcard, cc);
verifyFilteredOptionalAddress(postcard, opt).ifPresent(address -> message.addCc(address));
}
for (Address bcc : postcard.getBccList()) {
resolveAddressPersonalLabel(postcard, bcc);
final OptionalThing opt = addressFilter.filterBcc(postcard, bcc);
verifyFilteredOptionalAddress(postcard, opt).ifPresent(address -> message.addBcc(address));
}
final List replyToList = postcard.getReplyToList();
if (!replyToList.isEmpty()) {
final List filteredList = new ArrayList(replyToList.size());
for (Address replyTo : replyToList) {
resolveAddressPersonalLabel(postcard, replyTo);
final OptionalThing opt = addressFilter.filterReplyTo(postcard, replyTo);
verifyFilteredOptionalAddress(postcard, opt).ifPresent(address -> filteredList.add(address));
}
message.setReplyTo(filteredList);
}
}
// -----------------------------------------------------
// Label Handling
// --------------
protected void resolveAddressPersonalLabel(Postcard postcard, Address address) {
if (address instanceof InternetAddress) {
final InternetAddress internetAddress = (InternetAddress) address;
final String personal = internetAddress.getPersonal();
if (personal != null) {
final Locale locale = postcard.getReceiverLocale().orElse(Locale.getDefault());
final String resolved = labelStrategy.resolveLabel(postcard, locale, personal);
final String encoding = getPersonalEncoding();
try {
internetAddress.setPersonal(resolved, encoding);
} catch (UnsupportedEncodingException e) {
throw new SMailIllegalStateException("Unknown encoding for personal: " + encoding);
}
}
}
}
protected String getPersonalEncoding() {
return getBasicEncoding();
}
// -----------------------------------------------------
// Verify Address
// --------------
protected Address verifyFilteredFromAddress(Postcard postcard, Address filteredFrom) {
if (filteredFrom == null) {
String msg = "The filtered from-address should not be null: " + postcard;
throw new SMailIllegalStateException(msg);
}
return filteredFrom;
}
protected OptionalThing verifyFilteredOptionalAddress(Postcard postcard, OptionalThing opt) {
if (opt == null) {
String msg = "The filtered optional should not be null: postcard=" + postcard;
throw new SMailIllegalStateException(msg);
}
return opt;
}
protected void verifyFilteredToAddressExists(Postcard postcard, boolean existsToAddress) {
if (!existsToAddress) {
String msg = "Empty to-address by filtering: specifiedToAddress=" + postcard.getToList();
throw new SMailIllegalStateException(msg);
}
}
// ===================================================================================
// Prepare Subject
// ===============
protected void prepareSubject(Postcard postcard, SMailPostingMessage message) {
message.setSubject(getSubject(postcard), getSubjectEncoding());
}
protected String getSubject(Postcard postcard) {
return subjectFilter.filterSubject(postcard, postcard.getSubject().get());
}
protected String getSubjectEncoding() {
return getPersonalEncoding();
}
// ===================================================================================
// Prepare Body
// ============
protected void prepareBody(Postcard postcard, SMailPostingMessage message) {
final String plainText = toCompletePlainText(postcard);
final OptionalThing optHtmlText = toCompleteHtmlText(postcard);
message.savePlainTextForDisplay(plainText);
message.saveHtmlTextForDisplay(optHtmlText);
final Map attachmentMap = postcard.getAttachmentMap();
final MimeMessage nativeMessage = message.getMimeMessage();
if (attachmentMap.isEmpty()) { // normally here
setupTextPart(nativeMessage, plainText, TextType.PLAIN); // plain is required
optHtmlText.ifPresent(htmlText -> {
setupTextPart(nativeMessage, htmlText, TextType.HTML);
});
} else { // with attachment
if (optHtmlText.isPresent()) {
throw new SMailIllegalStateException("Unsupported HTML mail with attachment for now: " + postcard);
}
try {
final MimeMultipart multipart = createTextWithAttachmentMultipart(postcard, message, plainText, attachmentMap);
nativeMessage.setContent(multipart);
} catch (MessagingException e) {
String msg = "Failed to set attachment multipart content: " + postcard;
throw new SMailIllegalStateException(msg, e);
}
}
}
protected String toCompletePlainText(Postcard postcard) {
return postcard.toCompletePlainText().map(plainText -> {
return bodyTextFilter.filterBody(postcard, plainText, /*html*/false);
}).get();
}
protected OptionalThing toCompleteHtmlText(Postcard postcard) {
return postcard.toCompleteHtmlText().map(htmlText -> {
return bodyTextFilter.filterBody(postcard, htmlText, /*html*/true);
});
}
protected MimeMultipart createTextWithAttachmentMultipart(Postcard postcard, SMailPostingMessage message, String plain,
Map attachmentMap) throws MessagingException {
final MimeMultipart multipart = newMimeMultipart();
multipart.setSubType("mixed");
multipart.addBodyPart((BodyPart) setupTextPart(newMimeBodyPart(), plain, TextType.PLAIN));
for (Entry entry : attachmentMap.entrySet()) {
final SMailAttachment attachment = entry.getValue();
multipart.addBodyPart((BodyPart) setupAttachmentPart(message, attachment));
}
return multipart;
}
protected MimeMultipart newMimeMultipart() {
return new MimeMultipart();
}
protected MimeBodyPart newMimeBodyPart() {
return new MimeBodyPart();
}
// ===================================================================================
// Text Part
// =========
protected MimePart setupTextPart(MimePart part, String text, TextType textType) {
assertArgumentNotNull("part", part);
assertArgumentNotNull("text", text);
assertArgumentNotNull("textType", textType);
final String encoding = getTextEncoding();
final ByteBuffer buffer = prepareTextByteBuffer(text, encoding);
final DataSource source = prepareTextDataSource(buffer);
try {
part.setDataHandler(createDataHandler(source));
part.setHeader("Content-Transfer-Encoding", "7bit");
part.setHeader("Content-Type", "text/" + textType.code() + "; charset=\"" + encoding + "\"");
} catch (MessagingException e) {
throw new SMailMessageSettingFailureException("Failed to set headers: " + encoding, e);
}
return part;
}
protected String getTextEncoding() {
return getPersonalEncoding();
}
protected ByteBuffer prepareTextByteBuffer(String text, final String encoding) {
final ByteBuffer buffer;
try {
buffer = ByteBuffer.wrap(text.getBytes(encoding));
} catch (UnsupportedEncodingException e) {
throw new SMailMessageSettingFailureException("Unknown encoding: " + encoding, e);
}
return buffer;
}
protected ByteArrayDataSource prepareTextDataSource(final ByteBuffer buffer) {
return new ByteArrayDataSource(buffer.array(), "application/octet-stream");
}
protected static enum TextType {
PLAIN("plain"), HTML("html");
private final String code;
private TextType(String code) {
this.code = code;
}
public String code() {
return code;
}
}
// ===================================================================================
// Attachment Part
// ===============
protected MimePart setupAttachmentPart(SMailPostingMessage message, SMailAttachment attachment) {
assertArgumentNotNull("message", message);
assertArgumentNotNull("attachment", attachment);
final MimePart part = newMimeBodyPart();
final String textEncoding = getAttachmentTextEncoding(attachment);
final String contentType = buildAttachmentContentType(message, attachment, textEncoding);
final DataSource source = prepareAttachmentDataSource(message, attachment, textEncoding);
try {
part.setDataHandler(createDataHandler(source));
part.setHeader("Content-Transfer-Encoding", "base64");
part.addHeader("Content-Type", contentType);
part.addHeader("Content-Disposition", "attachment; filename=\"" + attachment.getFilenameOnHeader() + "\"");
} catch (MessagingException e) {
throw new SMailMessageSettingFailureException("Failed to set headers: " + attachment, e);
}
return part;
}
protected String buildAttachmentContentType(SMailPostingMessage message, SMailAttachment attachment, String textEncoding) {
final String filenameEncoding = getAttachmentFilenameEncoding();
final String encodedFilename;
try {
final String filename = attachment.getFilenameOnHeader();
encodedFilename = MimeUtility.encodeText(filename, filenameEncoding, "B"); // uses 'B' for various characters
} catch (UnsupportedEncodingException e) {
throw new SMailMessageSettingFailureException("Unknown encoding: " + filenameEncoding, e);
}
final StringBuilder sb = new StringBuilder();
final String contentType = attachment.getContentType();
sb.append(contentType);
if (contentType.equals("text/plain")) {
sb.append("; charset=").append(textEncoding);
}
sb.append("; name=\"").append(encodedFilename).append("\"");
return sb.toString();
}
protected DataSource prepareAttachmentDataSource(SMailPostingMessage message, SMailAttachment attachment, String textEncoding) {
final byte[] attachedBytes = readAttachedBytes(message, attachment);
message.saveAttachmentForDisplay(attachment, attachedBytes, textEncoding);
return new ByteArrayDataSource(attachedBytes, "application/octet-stream");
}
protected byte[] readAttachedBytes(SMailPostingMessage message, SMailAttachment attachment) {
final InputStream ins = attachment.getReourceStream();
AccessibleByteArrayOutputStream ous = null;
try {
ous = new AccessibleByteArrayOutputStream();
final byte[] buffer = new byte[8192];
int length;
while ((length = ins.read(buffer)) > 0) {
ous.write(buffer, 0, length);
}
return ous.getBytes();
} catch (IOException e) {
throw new SMailIllegalStateException("Failed to read the attached stream as bytes: " + attachment);
} finally {
if (ous != null) {
try {
ous.close();
} catch (IOException ignored) {}
}
try {
ins.close();
} catch (IOException ignored) {}
}
}
protected static class AccessibleByteArrayOutputStream extends ByteArrayOutputStream {
public byte[] getBytes() {
return buf;
}
}
protected String getAttachmentTextEncoding(SMailAttachment attachment) {
return attachment.getTextEncoding().get(); // always exists if text/plain
}
protected String getAttachmentFilenameEncoding() {
return getPersonalEncoding();
}
// ===================================================================================
// Send Message
// ============
protected void send(Postcard postcard, SMailPostingMessage message) {
try {
if (postcard.isAsync()) {
asyncStrategy.async(postcard, () -> doSend(postcard, message));
} else {
doSend(postcard, message);
}
} catch (RuntimeException e) {
if (postcard.isSuppressSendFailure()) {
loggingStrategy.logSuppressedCause(postcard, message, e);
} else {
throw e;
}
}
}
protected void doSend(Postcard postcard, SMailPostingMessage message) {
logMailMessage(postcard, message);
if (!training) {
retryableSend(postcard, message);
}
}
// -----------------------------------------------------
// Logging
// -------
protected void logMailMessage(Postcard postcard, SMailPostingMessage message) {
loggingStrategy.logMailMessage(postcard, message); // you can also make EML file here by overriding
}
// -----------------------------------------------------
// Retryable
// ---------
protected void retryableSend(Postcard postcard, SMailPostingMessage message) {
final int retryCount = getRetryCount(postcard); // not negative, zero means no retry
final long intervalMillis = getIntervalMillis(postcard); // not negative
int challengeCount = 0;
Exception firstCause = null;
while (true) {
if (challengeCount > retryCount) { // over retry limit, cannot send
if (firstCause != null) { // just in case
handleSendFailure(postcard, message, firstCause);
}
break;
}
try {
if (challengeCount > 0) { // means retry sending
waitBeforeRetrySending(intervalMillis);
}
actuallySend(message);
if (challengeCount > 0) { // means retry success
noticeRetrySuccess(postcard, message, challengeCount, firstCause);
}
break;
} catch (RuntimeException | MessagingException e) {
if (firstCause == null) { // first cause may be most important
firstCause = e;
}
}
++challengeCount;
}
}
protected int getRetryCount(Postcard postcard) { // you can override if all mails needs retry
return postcard.getRetryCount();
}
protected long getIntervalMillis(Postcard postcard) { // you can as well
return postcard.getIntervalMillis();
}
protected void waitBeforeRetrySending(long intervalMillis) {
if (intervalMillis > 0) {
try {
Thread.sleep(intervalMillis);
} catch (InterruptedException ignored) {}
}
}
protected void actuallySend(SMailPostingMessage message) throws MessagingException {
Transport.send(message.getMimeMessage());
}
protected void noticeRetrySuccess(Postcard postcard, SMailPostingMessage message, int challengeCount, Exception firstCause) {
loggingStrategy.logRetrySuccess(postcard, message, challengeCount, firstCause);
}
protected void handleSendFailure(Postcard postcard, SMailPostingMessage message, Exception e) {
final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
br.addNotice("Failed to send the mail message.");
br.addItem("Postcard");
br.addElement(postcard);
br.addItem("Posting Message");
br.addElement(Integer.hashCode(message.hashCode()));
br.addElement(message);
final String msg = br.buildExceptionMessage();
throw new SMailTransportFailureException(msg, e);
}
// ===================================================================================
// Assist Logic
// ============
protected String getBasicEncoding() {
return "UTF-8";
}
protected DataHandler createDataHandler(DataSource source) {
return new DataHandler(source);
}
// ===================================================================================
// General Helper
// ==============
protected void assertArgumentNotNull(String variableName, Object value) {
if (variableName == null) {
throw new IllegalArgumentException("The variableName should not be null.");
}
if (value == null) {
throw new IllegalArgumentException("The argument '" + variableName + "' should not be null.");
}
}
// ===================================================================================
// Accessor
// ========
public boolean isTraining() {
return training;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy