net.emphased.malle.javamail.JavamailMessage Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of malle-javamail Show documentation
Show all versions of malle-javamail Show documentation
Malle implementation based on Javamail
The newest version!
package net.emphased.malle.javamail;
import net.emphased.malle.*;
import javax.activation.DataHandler;
import javax.annotation.Nullable;
import javax.mail.*;
import javax.mail.internet.*;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import static net.emphased.malle.util.Preconditions.checkArgument;
import static net.emphased.malle.util.Preconditions.checkNotNull;
class JavamailMessage implements Mail {
private final MultipartMode multipartMode;
private final Javamail javamail;
private Charset charset = DEFAULT_CHARSET;
private final Map bodies = new EnumMap<>(BodyType.class);
private Encoding bodyEncoding = DEFAULT_BODY_ENCODING;
private Encoding attachmentEncoding = DEFAULT_ATTACHMENT_ENCODING;
private final Map> addresses = new EnumMap<>(AddressType.class);
private final Map> attachments = new EnumMap<>(Attachment.Type.class);
private String id;
private Integer priority;
private String subject;
private final Map headers = new LinkedHashMap<>();
private static final Map ENCODING_TO_RFC;
static {
EnumMap m = new EnumMap<>(Encoding.class);
m.put(Encoding.BASE64, "base64");
m.put(Encoding.QUOTED_PRINTABLE, "quoted-printable");
m.put(Encoding.EIGHT_BIT, "8bit");
m.put(Encoding.SEVEN_BIT, "7bit");
m.put(Encoding.BINARY, "binary");
ENCODING_TO_RFC = Collections.unmodifiableMap(m);
if (Encoding.values().length != ENCODING_TO_RFC.size()) {
throw new AssertionError("Not all Encoding values have mappings in ENCODING_TO_RFC");
}
}
JavamailMessage(Javamail javamail, MultipartMode multipartMode) {
checkNotNull(javamail, "The 'javamail' can't be null");
checkNotNull(multipartMode, "The 'multipartMode' can't be null");
this.multipartMode = multipartMode;
this.javamail = javamail;
}
MimeMessage getMimeMessage() {
return new MimeMessageBuilder().build();
}
@Override
public Mail charset(Charset charset) {
checkNotNull(charset, "The 'charset' can't be null");
this.charset = charset;
return this;
}
@Override
public Mail charset(String charset) {
charset(Charset.forName(charset));
return this;
}
@Override
public Mail bodyEncoding(@Nullable Encoding encoding) {
bodyEncoding = encoding;
return this;
}
@Override
public Mail attachmentEncoding(Encoding encoding) {
checkNotNull(encoding, "The 'encoding' must not be null");
this.attachmentEncoding = encoding;
return this;
}
@Override
public Mail id(String id) {
checkNotNull(id, "The 'id' can't be null");
this.id = id;
return this;
}
@Override
public Mail priority(int priority) {
this.priority = priority;
return this;
}
@Override
public Mail from(Iterable addresses) {
return address(AddressType.FROM, addresses);
}
@Override
public Mail from(String[] addresses) {
return address(AddressType.FROM, addresses);
}
@Override
public Mail from(String addresses) {
return address(AddressType.FROM, addresses);
}
@Override
public Mail from(String address, @Nullable String personal) {
return address(AddressType.FROM, address, personal);
}
@Override
public Mail replyTo(Iterable addresses) {
return address(AddressType.REPLY_TO, addresses);
}
@Override
public Mail replyTo(String[] addresses) {
return address(AddressType.REPLY_TO, addresses);
}
@Override
public Mail replyTo(String addresses) {
return address(AddressType.REPLY_TO, addresses);
}
@Override
public Mail replyTo(String address, @Nullable String personal) {
return address(AddressType.REPLY_TO, address, personal);
}
@Override
public Mail to(Iterable addresses) {
return address(AddressType.TO, addresses);
}
@Override
public Mail to(String[] addresses) {
return address(AddressType.TO, addresses);
}
@Override
public Mail to(String addresses) {
return address(AddressType.TO, addresses);
}
@Override
public Mail to(String address, @Nullable String personal) {
return address(AddressType.TO, address, personal);
}
@Override
public Mail cc(Iterable addresses) {
return address(AddressType.CC, addresses);
}
@Override
public Mail cc(String[] addresses) {
return address(AddressType.CC, addresses);
}
@Override
public Mail cc(String addresses) {
return address(AddressType.CC, addresses);
}
@Override
public Mail cc(String address, @Nullable String personal) {
return address(AddressType.CC, address, personal);
}
@Override
public Mail bcc(Iterable addresses) {
return address(AddressType.BCC, addresses);
}
public Mail bcc(String addresses) {
return address(AddressType.BCC, addresses);
}
@Override
public Mail bcc(String[] addresses) {
return address(AddressType.BCC, addresses);
}
@Override
public Mail bcc(String address, @Nullable String personal) {
return address(AddressType.BCC, address, personal);
}
@Override
public Mail address(AddressType type, Iterable addresses) {
for (String a: addresses) {
address(type, a);
}
return this;
}
@Override
public Mail address(AddressType type, String[] addresses) {
return address(type, Utils.toIterable(addresses));
}
@Override
public Mail address(AddressType type, String addresses) {
for(InternetAddress ia: parseAddresses(addresses)) {
address(type, ia);
}
return this;
}
@Override
public Mail address(AddressType type, String address, @Nullable String personal) {
return address(type, createAddress(address, personal));
}
@Override
public Mail subject(String subject) {
checkNotNull(subject, "The 'subject' must not be null");
this.subject = subject;
return this;
}
@Override
public Mail header(String name, String value) {
checkNotNull(name, "The 'name' must not be null");
checkNotNull(value, "The 'value' must not be null");
headers.put(name, value);
return this;
}
@Override
public Mail plain(String plain) {
checkNotNull(plain, "The 'plain' must not be null");
return body(BodyType.PLAIN, plain);
}
@Override
public Mail html(String html) {
checkNotNull(html, "The 'html' must not be null");
return body(BodyType.HTML, html);
}
@Override
public Mail body(BodyType type, String value) {
checkNotNull(type, "The 'type' must not be null");
checkNotNull(value, "The 'value' must not be null");
if (!isMultipart() && !bodies.isEmpty() && !bodies.containsKey(type)) {
checkMultipart();
}
bodies.put(type, value);
return this;
}
@Override
public Mail attachment(InputStreamSupplier content, String name, @Nullable String type) {
return attachment(Attachment.Type.REGULAR, content, null, name, type);
}
@Override
public Mail attachment(InputStreamSupplier content, String name) {
return attachment(content, name, null);
}
@Override
public Mail inline(InputStreamSupplier content, String id, @Nullable String type) {
return attachment(Attachment.Type.INLINE, content, id, null, type);
}
private Mail attachment(Attachment.Type type, InputStreamSupplier content, @Nullable String id,
@Nullable String name, @Nullable String contentType) {
checkNotNull(content, "The 'content' can't be null");
if (type == Attachment.Type.INLINE) {
checkNotNull(id, "The 'id' can't be null");
} else {
checkNotNull(name, "The 'name' can't be null");
}
if (contentType == null) {
contentType = "application/octet-stream";
}
checkMultipart();
List list = attachments.get(type);
if (list == null) {
list = new ArrayList<>();
attachments.put(type, list);
}
list.add(new Attachment(type, content, id, name, contentType));
return this;
}
@Override
public Mail inline(InputStreamSupplier content, String id) {
return inline(content, id, "application/octet-stream");
}
@Override
public Mail template(String name, @Nullable Locale locale, Map context) {
checkNotNull(name, "The 'name' can't be null");
checkNotNull(context, "The 'context' can't be null");
javamail.applyTemplate(this, name, locale, context);
return this;
}
@Override
public Mail template(String name, Map context) {
return template(name, null, context);
}
@Override
public Mail template(String name, @Nullable Locale locale, Object... context) {
checkNotNull(name, "The 'name' can't be null");
checkNotNull(context, "The 'context' can't be null");
checkArgument(context.length % 2 == 0, "The 'context' varargs must contain an even number of values");
Map contextMap;
if (context.length != 0) {
int len = context.length / 2;
contextMap = new HashMap<>(len);
for (int i = 0; i < len; i++) {
Object key = context[i * 2];
if (!(key instanceof String)) {
checkArgument(false, "The keys in 'context' must be of String type");
}
Object value = context[i * 2 + 1];
contextMap.put((String) key, value);
}
} else {
contextMap = Collections.emptyMap();
}
return template(name, locale, contextMap);
}
@Override
public Mail template(String name, Object... context) {
return template(name, null, context);
}
@Override
public Mail send() {
javamail.send(this);
return this;
}
@Override
public Mail writeTo(OutputStream outputStream) {
try {
getMimeMessage().writeTo(outputStream);
} catch (IOException e) {
throw new MailIOException(e);
} catch (MessagingException e) {
throw Utils.wrapException(e);
}
return this;
}
@Override
public Mail writeTo(Path path) {
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(path.toFile()))) {
return writeTo(os);
} catch (IOException e) {
throw new MailIOException(e);
}
}
@Override
public Mail writeTo(String path) {
return writeTo(Paths.get(path));
}
private Mail address(AddressType type, InternetAddress address) {
List l = addresses.get(type);
if (l == null) {
l = new ArrayList<>();
addresses.put(type, l);
}
l.add(address);
return this;
}
private InternetAddress[] parseAddresses(String addresses) {
try {
InternetAddress[] r = InternetAddress.parse(addresses, false);
for (int i = 0; i < r.length; i++) {
InternetAddress ia = r[i];
String personal = ia.getPersonal();
// Force recoding of the personal part. This will also encode a possibly
// unencoded personal that needs encoding because of the non US-ASCII characters.
r[i] = new InternetAddress(ia.getAddress(), personal, charset.name());
}
return r;
} catch (AddressException e) {
throw Utils.wrapException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Shouldn't happen", e);
}
}
private InternetAddress createAddress(String address, @Nullable String personal) {
checkNotNull(address, "The 'address' can't be null");
InternetAddress r;
try {
r = new InternetAddress(address, personal, charset.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Shouldn't happen", e);
}
try {
r.validate();
} catch (AddressException e) {
throw Utils.wrapException(e);
}
return r;
}
private class MimeMessageBuilder {
private MimeMessage root;
private final Map attachmentParts = new EnumMap<>(Attachment.Type.class);
private void buildParts() throws MessagingException {
root = new MimeMessage((Session) null);
MimeMultipart attachmentPart, inlinePart;
switch (multipartMode) {
case NONE:
attachmentPart = null;
inlinePart = null;
break;
case MIXED:
attachmentPart = new MimeMultipart("mixed");
root.setContent(attachmentPart);
inlinePart = attachmentPart;
break;
case RELATED:
attachmentPart = new MimeMultipart("related");
root.setContent(attachmentPart);
inlinePart = attachmentPart;
break;
case MIXED_RELATED:
attachmentPart = new MimeMultipart("mixed");
root.setContent(attachmentPart);
inlinePart = new MimeMultipart("related");
MimeBodyPart relatedBodyPart = new MimeBodyPart();
relatedBodyPart.setContent(inlinePart);
attachmentPart.addBodyPart(relatedBodyPart);
break;
default:
throw new AssertionError("Unhandled MultipartMode: " + multipartMode);
}
attachmentParts.put(Attachment.Type.REGULAR, attachmentPart);
attachmentParts.put(Attachment.Type.INLINE, inlinePart);
}
private void buildAddresses() throws MessagingException {
for (Map.Entry> entry: addresses.entrySet()) {
List l = entry.getValue();
switch (entry.getKey()) {
case FROM:
root.removeHeader("From");
if (!l.isEmpty()) {
root.addFrom(Utils.toArray(l, InternetAddress.class));
}
break;
case REPLY_TO:
if (!l.isEmpty()) {
root.setReplyTo(Utils.toArray(l, InternetAddress.class));
}
break;
case TO:
root.removeHeader("To");
if (!l.isEmpty()) {
root.addRecipients(Message.RecipientType.TO, Utils.toArray(l, InternetAddress.class));
}
break;
case CC:
root.removeHeader("CC");
if (!l.isEmpty()) {
root.addRecipients(Message.RecipientType.CC, Utils.toArray(l, InternetAddress.class));
}
break;
case BCC:
root.removeHeader("BCC");
if (!l.isEmpty()) {
root.addRecipients(Message.RecipientType.BCC, Utils.toArray(l, InternetAddress.class));
}
break;
default:
throw new AssertionError("Unhandled RecipientType: " + entry.getKey());
}
}
}
private MimeBodyPart getOrCreateTextPart() throws MessagingException {
MimeMultipart container = attachmentParts.get(Attachment.Type.INLINE);
if (container.getCount() == 0) {
MimeBodyPart part = new MimeBodyPart();
container.addBodyPart(part);
return part;
} else {
return (MimeBodyPart) container.getBodyPart(0);
}
}
private void setText(String plain, String html) throws MessagingException {
MimeMultipart messageBody = new MimeMultipart("alternative");
getOrCreateTextPart().setContent(messageBody, "text/alternative");
// Create the plain text part of the message.
MimeBodyPart plainTextPart = new MimeBodyPart();
setPlainTextToMimePart(plainTextPart, plain);
messageBody.addBodyPart(plainTextPart);
// Create the HTML text part of the message.
MimeBodyPart htmlTextPart = new MimeBodyPart();
setHtmlToMimePart(htmlTextPart, html);
messageBody.addBodyPart(htmlTextPart);
}
private void setText(String text, boolean html) throws MessagingException {
MimePart partToUse;
if (isMultipart()) {
partToUse = getOrCreateTextPart();
} else {
partToUse = root;
}
if (html) {
setHtmlToMimePart(partToUse, text);
} else {
setPlainTextToMimePart(partToUse, text);
}
}
private void setPlainTextToMimePart(MimePart part, String text) throws MessagingException {
setTextToMimePart(part, text, "text/plain", bodyEncoding);
}
private void setHtmlToMimePart(MimePart part, String text) throws MessagingException {
setTextToMimePart(part, text, "text/html", bodyEncoding);
}
private void setTextToMimePart(MimePart mimePart, String text, String type,
@Nullable Encoding encoding) throws MessagingException {
mimePart.setContent(text, type + "; charset=" + charset.name());
setContentTransferEncodingHeader(mimePart, encoding);
}
private void buildBodies() throws MessagingException {
String plain = bodies.get(BodyType.PLAIN);
String html = bodies.get(BodyType.HTML);
if (plain != null && html != null) {
checkMultipart();
setText(plain, html);
} else if (plain != null) {
setText(plain, false);
} else if (html != null) {
setText(html, true);
} else {
throw new IllegalStateException("The message must have plain and/or html text set");
}
}
private void buildAttachments() throws MessagingException {
for (Map.Entry> entry: attachments.entrySet()) {
MimeMultipart target = attachmentParts.get(entry.getKey());
for (Attachment att: entry.getValue()) {
BodyPart part = att.createBodyPart(attachmentEncoding, charset);
target.addBodyPart(part);
}
}
}
public MimeMessage build() {
root = null;
attachmentParts.clear();
try {
buildParts();
try {
for (Map.Entry header : headers.entrySet()) {
root.addHeader(header.getKey(), MimeUtility.fold(header.getKey().length() + 2,
MimeUtility.encodeText(header.getValue(), charset.name(), null)));
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Shouldn't happen", e);
}
if (priority != null) {
root.setHeader("X-Priority", String.valueOf(priority));
}
if (subject != null) {
root.setSubject(subject, charset.name());
}
buildAddresses();
buildBodies();
buildAttachments();
root.saveChanges();
// Doing this after saveChanges() because the latter overwrites Message-ID header.
if (id != null) {
root.setHeader("Message-ID", '<' + id + '>');
}
} catch (MessagingException e) {
throw Utils.wrapException(e);
}
return root;
}
}
private static void setContentTransferEncodingHeader(Part part, @Nullable Encoding encoding) throws MessagingException {
if (encoding != null) {
part.setHeader("Content-Transfer-Encoding", checkNotNull(ENCODING_TO_RFC.get(encoding)));
} else {
part.removeHeader("Content-Transfer-Encoding");
}
}
private boolean isMultipart() {
return multipartMode != MultipartMode.NONE;
}
private void checkMultipart() {
if (!isMultipart()) {
throw new UnsupportedOperationException("The requested feature requires a multipart message. Please use MailSystem.mail([true])");
}
}
private static class Attachment {
private enum Type {
REGULAR, INLINE
}
private final Type type;
private final InputStreamSupplier content;
private final String id;
private final String name;
private final String contentType;
public Attachment(Type type, InputStreamSupplier content,
@Nullable String id, @Nullable String name, String contentType) {
this.type = type;
this.content = content;
this.id = id;
this.name = name;
this.contentType = contentType;
}
public BodyPart createBodyPart(Encoding encoding, @Nullable Charset charset) throws MessagingException {
MimeBodyPart r = new MimeBodyPart();
r.setDisposition(type == Type.INLINE ? MimeBodyPart.INLINE : MimeBodyPart.ATTACHMENT);
if (id != null) {
r.setContentID('<' + id + '>');
}
if (name != null) {
checkNotNull(charset);
try {
r.setFileName(MimeUtility.encodeWord(name, charset.name(), null));
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("Shouldn't happen", ex);
}
}
r.setDataHandler(new DataHandler(new InputStreamSupplierDatasource(content, contentType,
type == Type.INLINE ? "inline" : name)));
setContentTransferEncodingHeader(r, encoding);
return r;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy