/*
* Copyright (C) 2012 Works Applications Co., Ltd.
* http://www.worksap.co.jp/
*
* Licensed under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
*/
package jp.co.worksap.message.wrapper;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map.Entry;
import javax.activation.DataHandler;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.mail.Address;
import javax.mail.Flags;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import jp.co.worksap.message.decoder.ContentDecoder;
import jp.co.worksap.message.decoder.HeaderDecoder;
import jp.co.worksap.message.parser.AddressParser;
import jp.co.worksap.message.parser.AttachedFileParser;
import jp.co.worksap.message.parser.ContentParser;
import jp.co.worksap.message.util.CharsetUtility;
import jp.co.worksap.message.util.Encoding;
import jp.co.worksap.message.util.StringValidator;
public class MessageWrapper extends Message {
static {
System.setProperty("sun.nio.cs.map", "x-windows-iso2022jp/ISO-2022-JP");
System.setProperty("mail.mime.contenttypehandler", "jp.co.worksap.message.util.CharsetMap");
}
private static final String REPLY_TO = "Reply-To";
@Nonnull
private final Message instance;
@Nonnull
private String UID;
private boolean isFileNameFixed = false;
public MessageWrapper(@Nullable Session session, @Nonnull InputStream is) {
checkNotNull(is);
try {
this.instance = new MimeMessage(session, is);
} catch (MessagingException e) {
throw new RuntimeException("Failed to crate MessageWrapper", e);
}
}
public MessageWrapper(@Nonnull Session session) {
checkNotNull(session);
this.instance = new MimeMessage(session);
}
@Override
public void addHeader(String headerName, String headerValue)
throws MessagingException {
instance.addHeader(headerName, headerValue);
}
@Override
public Enumeration<?> getAllHeaders() throws MessagingException {
return instance.getAllHeaders();
}
@Override
public Address[] getAllRecipients() throws MessagingException {
return instance.getAllRecipients();
}
@Override
public void addRecipient(RecipientType type, Address address)
throws MessagingException {
instance.addRecipient(type, address);
}
@Override
public Address[] getReplyTo() throws MessagingException {
String[] replyTo = getHeader(REPLY_TO);
AddressParser parser = new AddressParser();
if (parser.isBlankStrings(replyTo)) {
return getFrom();
}
List<Address> addresses = new ArrayList<Address>();
for (String t : replyTo) {
String[] splitted = t.split(",");
for (String s : splitted) {
addresses.add(parser.getAddressFromRawString(s));
}
}
if (addresses.size() == 0) {
return getFrom();
}
return addresses.toArray(new Address[0]);
}
@Override
public Object getContent() throws IOException, MessagingException {
if (instance instanceof MimeMessage) {
ContentParser parser = new ContentParser((MimeMessage) instance);
if (parser.isMimeMessageIncludingNoCharset()) {
CharsetGuesser guesser = new CharsetGuesser();
String charset = guesser.guessCharset(instance.getAllHeaders(),
((MimeMessage) instance).getRawInputStream());
return parser.parseContent(charset);
}
if (parser.isQuotedPrintableShiftJisContent()) {
return parser.parseContent(Encoding.MS932);
}
}
Object content = null;
try {
content = getContentFromMessage();
} catch (IOException e) {
// Message.getContent() throws IOException if the content was
// broken.
// This method tries to decode it by ignoring
// Content-Transfer-Encoding.
// If the instance is MimeMessage, the method tries to decode it.
// If succeeds, the method returns decoded string and
// don't throw any exceptions.
if (instance instanceof MimeMessage) {
ContentParser parser = new ContentParser((MimeMessage) instance);
ContentDecoder decoder = new ContentDecoder();
return decoder.decodeContent(
((MimeMessage) instance).getRawInputStream(),
parser.getCharset(), "8bit");
}
// if these trial is failed, throw exception.
throw e;
}
if (!(content instanceof Multipart)) {
return instance.getContent();
}
if (!isFileNameFixed) {
// This is multipart and attached file name is often broken.
// This method repairs them and repackage them.
AttachedFileParser parser = new AttachedFileParser(instance);
parser.fixFileName((Multipart) content);
isFileNameFixed = true;
}
return content;
}
@Override
public String getContentType() throws MessagingException {
return instance.getContentType();
}
@Override
public DataHandler getDataHandler() throws MessagingException {
return instance.getDataHandler();
}
@Override
public String getDescription() throws MessagingException {
return instance.getDescription();
}
@Override
public String getDisposition() throws MessagingException {
return instance.getDisposition();
}
@Override
public String getFileName() throws MessagingException {
if (instance instanceof MimeMessage
&& instance.isMimeType("application/*")) {
String[] disposition = instance.getHeader("Content-Disposition");
String s = null;
if (disposition != null && disposition.length > 0) {
s = "\r\ncontent-disposition: " + disposition[0];
}
if (s == null) {
s = instance.getContentType();
}
if (s != null) {
if (isFileNameFixed) {
return instance.getFileName();
}
return (new HeaderDecoder()).decodeFileName(s);
}
}
return instance.getFileName();
}
@Override
public String[] getHeader(String headerName) throws MessagingException {
return instance.getHeader(headerName);
}
@Override
public InputStream getInputStream() throws IOException, MessagingException {
return instance.getInputStream();
}
@Override
public int getLineCount() throws MessagingException {
return instance.getLineCount();
}
@Override
public Enumeration<?> getMatchingHeaders(String[] headerName)
throws MessagingException {
return instance.getMatchingHeaders(headerName);
}
@Override
public Enumeration<?> getNonMatchingHeaders(String[] headerName)
throws MessagingException {
return instance.getNonMatchingHeaders(headerName);
}
@Override
public int getSize() throws MessagingException {
return instance.getSize();
}
@Override
public boolean isMimeType(String mimeType) throws MessagingException {
return instance.isMimeType(mimeType);
}
@Override
public void removeHeader(String headerName) throws MessagingException {
instance.removeHeader(headerName);
}
@Override
public void setContent(Multipart mp) throws MessagingException {
instance.setContent(mp);
}
@Override
public void setContent(Object obj, String type) throws MessagingException {
instance.setContent(obj, type);
}
@Override
public void setDataHandler(DataHandler dh) throws MessagingException {
instance.setDataHandler(dh);
}
@Override
public void setDescription(String description) throws MessagingException {
instance.setDescription(description);
}
@Override
public void setDisposition(String disposition) throws MessagingException {
instance.setDisposition(disposition);
}
@Override
public void setFileName(String fileName) throws MessagingException {
instance.setFileName(fileName);
}
@Override
public void setHeader(String headerName, String headerValue)
throws MessagingException {
instance.setHeader(headerName, headerValue);
}
@Override
public void setText(String text) throws MessagingException {
instance.setText(text);
}
@Override
public void writeTo(OutputStream os) throws IOException, MessagingException {
instance.writeTo(os);
}
@Override
public void addFrom(Address[] addresses) throws MessagingException {
instance.addFrom(addresses);
}
@Override
public void addRecipients(RecipientType type, Address[] addresses)
throws MessagingException {
instance.addRecipients(type, addresses);
}
@Override
public Flags getFlags() throws MessagingException {
return instance.getFlags();
}
/**
* return "from" field. MessageWraper return Address[0] when the field is
* null. because no one need to distinguish "empty address field" and
* "no address field".
*
* FYI, rfc2822 says "from" field appears just 1 time and no value is
* unacceptable.
*/
@Override
public InternetAddress[] getFrom() throws MessagingException {
// rfc2822 says "from" field appears just 1 time.
// but Message class returns null when "from" field does not appear
Address[] garble = instance.getFrom();
String[] constructable = getHeader("from");
try {
AddressParser parser = new AddressParser();
return parser.fixAddress(garble, constructable);
} catch (MessagingException e) {
return cast(garble);
}
}
/**
* MessageWrapper.getSubject() don't distinguish "no subject field" and
* "no sentence subject"
*/
@Override
public String getSubject() throws MessagingException {
String[] subject = getHeader("subject");
if ((subject == null) || (subject[0] == null)) {
// rfc2822 says "subject field appears 0 or 1 time"
return "";
}
// don't care lower case or upper
String lower = subject[0].toLowerCase();
if (CharsetUtility.needsMapping(lower)) {
HeaderDecoder decoder = new HeaderDecoder();
String decoded = decoder.decodeSubject(subject[0]);
return decoded.replaceAll("\\r\\n", "");
}
String decoded = instance.getSubject();
// try to decode if it contains invalid char
if (!StringValidator.isValid(decoded)) {
CharsetGuesser guesser = new CharsetGuesser();
String charset = guesser.guessCharset(instance.getAllHeaders(),
new ByteArrayInputStream(subject[0].getBytes()));
if (!charset.isEmpty()) {
try {
return new String(subject[0].getBytes(), charset);
} catch (UnsupportedEncodingException e) {
throw new MessagingException("Unsupported encoding", e);
}
}
}
return decoded;
}
@Override
public Date getReceivedDate() throws MessagingException {
return instance.getReceivedDate();
}
/**
* return "to" or "cc" or "bcc" field. MessageWraper return Address[0] when
* the field is null. because no one need to distinguish
* "empty address field" and "no address field".
*
* FYI, rfc2822 says "to" and/or "cc" field appears 0 or 1 time.
*/
@Override
public InternetAddress[] getRecipients(RecipientType type)
throws MessagingException {
Address[] garble = instance.getRecipients(type);
String[] constructable = getHeader(type.toString());
if (garble == null || constructable == null) {
return new InternetAddress[0];
}
try {
AddressParser parser = new AddressParser();
return parser.fixAddress(garble, constructable);
} catch (MessagingException e) {
return cast(garble);
}
}
@Override
public Date getSentDate() throws MessagingException {
return instance.getSentDate();
}
@Override
public Message reply(boolean replyToAll) throws MessagingException {
return instance.reply(replyToAll);
}
@Override
public void saveChanges() throws MessagingException {
instance.saveChanges();
}
@Override
public void setFlags(Flags flag, boolean set) throws MessagingException {
instance.setFlags(flag, set);
}
@Override
public void setFrom() throws MessagingException {
instance.setFrom();
}
@Override
public void setFrom(Address address) throws MessagingException {
instance.setFrom(address);
}
@Override
public void setRecipients(RecipientType type, Address[] addresses)
throws MessagingException {
instance.setRecipients(type, addresses);
}
@Override
public void setSentDate(Date date) throws MessagingException {
instance.setSentDate(date);
}
@Override
public void setSubject(@Nullable String subject) throws MessagingException {
if (subject == null) {
instance.setSubject("");
} else {
instance.setSubject(subject);
}
}
@Nonnull
public String getUID() {
if (UID == null) {
throw new IllegalStateException("UID is not set yet.");
}
return UID;
}
public void setUID(@Nonnull String uid) {
this.UID = checkNotNull(uid);
}
private Object getContentFromMessage() throws IOException,
MessagingException {
try {
return instance.getContent();
} catch (UnsupportedEncodingException e) {
// fix the format of the "charset" field
String wrongCharset = e.getMessage().toLowerCase();
for (Entry<String, String> entry : CharsetUtility.getCharsetMap()
.entrySet()) {
if (wrongCharset.contains(entry.getKey())) {
String contentType = getContentType();
setHeader("Content-Type", contentType.split(";")[0]
+ "; charset=" + entry.getValue());
return instance.getContent();
}
}
throw e;
}
}
private InternetAddress[] cast(Address[] garble) {
InternetAddress[] casted = new InternetAddress[garble.length];
for (int i = 0; i < casted.length; ++i) {
casted[i] = (InternetAddress) garble[i];
}
return casted;
}
}