mirror of
https://github.com/moparisthebest/k-9
synced 2024-12-19 06:12:25 -05:00
1ff128890d
No actual impact, because all existing calls were with recurse = true.
677 lines
22 KiB
Java
677 lines
22 KiB
Java
|
|
package com.fsck.k9.mail.internet;
|
|
|
|
import java.io.BufferedWriter;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.OutputStreamWriter;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.Date;
|
|
import java.util.LinkedList;
|
|
import java.util.Locale;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
|
|
import org.apache.james.mime4j.MimeException;
|
|
import org.apache.james.mime4j.dom.field.DateTimeField;
|
|
import org.apache.james.mime4j.field.DefaultFieldParser;
|
|
import org.apache.james.mime4j.io.EOLConvertingInputStream;
|
|
import org.apache.james.mime4j.parser.ContentHandler;
|
|
import org.apache.james.mime4j.parser.MimeStreamParser;
|
|
import org.apache.james.mime4j.stream.BodyDescriptor;
|
|
import org.apache.james.mime4j.stream.Field;
|
|
import org.apache.james.mime4j.stream.MimeConfig;
|
|
import org.apache.james.mime4j.util.MimeUtil;
|
|
|
|
import com.fsck.k9.mail.Address;
|
|
import com.fsck.k9.mail.Body;
|
|
import com.fsck.k9.mail.BodyPart;
|
|
import com.fsck.k9.mail.CompositeBody;
|
|
import com.fsck.k9.mail.Message;
|
|
import com.fsck.k9.mail.MessagingException;
|
|
import com.fsck.k9.mail.Multipart;
|
|
import com.fsck.k9.mail.Part;
|
|
import com.fsck.k9.mail.store.UnavailableStorageException;
|
|
|
|
/**
|
|
* An implementation of Message that stores all of it's metadata in RFC 822 and
|
|
* RFC 2045 style headers.
|
|
*/
|
|
public class MimeMessage extends Message {
|
|
protected MimeHeader mHeader = new MimeHeader();
|
|
protected Address[] mFrom;
|
|
protected Address[] mTo;
|
|
protected Address[] mCc;
|
|
protected Address[] mBcc;
|
|
protected Address[] mReplyTo;
|
|
|
|
protected String mMessageId;
|
|
protected String[] mReferences;
|
|
protected String[] mInReplyTo;
|
|
|
|
protected Date mSentDate;
|
|
protected SimpleDateFormat mDateFormat;
|
|
|
|
protected Body mBody;
|
|
protected int mSize;
|
|
|
|
public MimeMessage() {
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse the given InputStream using Apache Mime4J to build a MimeMessage.
|
|
* Nested messages will not be recursively parsed.
|
|
*
|
|
* @param in
|
|
* @throws IOException
|
|
* @throws MessagingException
|
|
*
|
|
* @see #MimeMessage(InputStream in, boolean recurse)
|
|
*/
|
|
public MimeMessage(InputStream in) throws IOException, MessagingException {
|
|
parse(in);
|
|
}
|
|
|
|
/**
|
|
* Parse the given InputStream using Apache Mime4J to build a MimeMessage.
|
|
*
|
|
* @param in
|
|
* @param recurse A boolean indicating to recurse through all nested MimeMessage subparts.
|
|
* @throws IOException
|
|
* @throws MessagingException
|
|
*/
|
|
public MimeMessage(InputStream in, boolean recurse) throws IOException, MessagingException {
|
|
parse(in, recurse);
|
|
}
|
|
|
|
protected void parse(InputStream in) throws IOException, MessagingException {
|
|
parse(in, false);
|
|
}
|
|
|
|
protected void parse(InputStream in, boolean recurse) throws IOException, MessagingException {
|
|
mHeader.clear();
|
|
mFrom = null;
|
|
mTo = null;
|
|
mCc = null;
|
|
mBcc = null;
|
|
mReplyTo = null;
|
|
|
|
mMessageId = null;
|
|
mReferences = null;
|
|
mInReplyTo = null;
|
|
|
|
mSentDate = null;
|
|
|
|
mBody = null;
|
|
|
|
MimeConfig parserConfig = new MimeConfig();
|
|
parserConfig.setMaxHeaderLen(-1); // The default is a mere 10k
|
|
parserConfig.setMaxLineLen(-1); // The default is 1000 characters. Some MUAs generate
|
|
// REALLY long References: headers
|
|
parserConfig.setMaxHeaderCount(-1); // Disable the check for header count.
|
|
MimeStreamParser parser = new MimeStreamParser(parserConfig);
|
|
parser.setContentHandler(new MimeMessageBuilder());
|
|
if (recurse) {
|
|
parser.setRecurse();
|
|
}
|
|
try {
|
|
parser.parse(new EOLConvertingInputStream(in));
|
|
} catch (MimeException me) {
|
|
throw new Error(me);
|
|
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Date getSentDate() {
|
|
if (mSentDate == null) {
|
|
try {
|
|
DateTimeField field = (DateTimeField)DefaultFieldParser.parse("Date: "
|
|
+ MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
|
|
mSentDate = field.getDate();
|
|
} catch (Exception e) {
|
|
|
|
}
|
|
}
|
|
return mSentDate;
|
|
}
|
|
|
|
/**
|
|
* Sets the sent date object member as well as *adds* the 'Date' header
|
|
* instead of setting it (for performance reasons).
|
|
*
|
|
* @see #mSentDate
|
|
* @param sentDate
|
|
* @throws com.fsck.k9.mail.MessagingException
|
|
*/
|
|
public void addSentDate(Date sentDate) throws MessagingException {
|
|
if (mDateFormat == null) {
|
|
mDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
|
|
}
|
|
addHeader("Date", mDateFormat.format(sentDate));
|
|
setInternalSentDate(sentDate);
|
|
}
|
|
|
|
@Override
|
|
public void setSentDate(Date sentDate) throws MessagingException {
|
|
removeHeader("Date");
|
|
addSentDate(sentDate);
|
|
}
|
|
|
|
public void setInternalSentDate(Date sentDate) {
|
|
this.mSentDate = sentDate;
|
|
}
|
|
|
|
@Override
|
|
public String getContentType() throws MessagingException {
|
|
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
|
|
return (contentType == null) ? "text/plain" : contentType;
|
|
}
|
|
|
|
public String getDisposition() throws MessagingException {
|
|
return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
|
|
}
|
|
public String getContentId() throws MessagingException {
|
|
return null;
|
|
}
|
|
public String getMimeType() throws MessagingException {
|
|
return MimeUtility.getHeaderParameter(getContentType(), null);
|
|
}
|
|
|
|
public boolean isMimeType(String mimeType) throws MessagingException {
|
|
return getMimeType().equalsIgnoreCase(mimeType);
|
|
}
|
|
|
|
public int getSize() {
|
|
return mSize;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of the given recipient type from this message. If no addresses are
|
|
* found the method returns an empty array.
|
|
*/
|
|
@Override
|
|
public Address[] getRecipients(RecipientType type) throws MessagingException {
|
|
if (type == RecipientType.TO) {
|
|
if (mTo == null) {
|
|
mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
|
|
}
|
|
return mTo;
|
|
} else if (type == RecipientType.CC) {
|
|
if (mCc == null) {
|
|
mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
|
|
}
|
|
return mCc;
|
|
} else if (type == RecipientType.BCC) {
|
|
if (mBcc == null) {
|
|
mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
|
|
}
|
|
return mBcc;
|
|
} else {
|
|
throw new MessagingException("Unrecognized recipient type.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
|
|
if (type == RecipientType.TO) {
|
|
if (addresses == null || addresses.length == 0) {
|
|
removeHeader("To");
|
|
this.mTo = null;
|
|
} else {
|
|
setHeader("To", Address.toEncodedString(addresses));
|
|
this.mTo = addresses;
|
|
}
|
|
} else if (type == RecipientType.CC) {
|
|
if (addresses == null || addresses.length == 0) {
|
|
removeHeader("CC");
|
|
this.mCc = null;
|
|
} else {
|
|
setHeader("CC", Address.toEncodedString(addresses));
|
|
this.mCc = addresses;
|
|
}
|
|
} else if (type == RecipientType.BCC) {
|
|
if (addresses == null || addresses.length == 0) {
|
|
removeHeader("BCC");
|
|
this.mBcc = null;
|
|
} else {
|
|
setHeader("BCC", Address.toEncodedString(addresses));
|
|
this.mBcc = addresses;
|
|
}
|
|
} else {
|
|
throw new MessagingException("Unrecognized recipient type.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the unfolded, decoded value of the Subject header.
|
|
*/
|
|
@Override
|
|
public String getSubject() {
|
|
return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"), this);
|
|
}
|
|
|
|
@Override
|
|
public void setSubject(String subject) throws MessagingException {
|
|
setHeader("Subject", subject);
|
|
}
|
|
|
|
@Override
|
|
public Address[] getFrom() {
|
|
if (mFrom == null) {
|
|
String list = MimeUtility.unfold(getFirstHeader("From"));
|
|
if (list == null || list.length() == 0) {
|
|
list = MimeUtility.unfold(getFirstHeader("Sender"));
|
|
}
|
|
mFrom = Address.parse(list);
|
|
}
|
|
return mFrom;
|
|
}
|
|
|
|
@Override
|
|
public void setFrom(Address from) throws MessagingException {
|
|
if (from != null) {
|
|
setHeader("From", from.toEncodedString());
|
|
this.mFrom = new Address[] {
|
|
from
|
|
};
|
|
} else {
|
|
this.mFrom = null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Address[] getReplyTo() {
|
|
if (mReplyTo == null) {
|
|
mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
|
|
}
|
|
return mReplyTo;
|
|
}
|
|
|
|
@Override
|
|
public void setReplyTo(Address[] replyTo) throws MessagingException {
|
|
if (replyTo == null || replyTo.length == 0) {
|
|
removeHeader("Reply-to");
|
|
mReplyTo = null;
|
|
} else {
|
|
setHeader("Reply-to", Address.toEncodedString(replyTo));
|
|
mReplyTo = replyTo;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String getMessageId() throws MessagingException {
|
|
if (mMessageId == null) {
|
|
mMessageId = getFirstHeader("Message-ID");
|
|
}
|
|
if (mMessageId == null) { // even after checking the header
|
|
setMessageId(generateMessageId());
|
|
}
|
|
return mMessageId;
|
|
}
|
|
|
|
private String generateMessageId() {
|
|
return "<" + UUID.randomUUID().toString() + "@email.android.com>";
|
|
}
|
|
|
|
public void setMessageId(String messageId) throws UnavailableStorageException {
|
|
setHeader("Message-ID", messageId);
|
|
mMessageId = messageId;
|
|
}
|
|
|
|
@Override
|
|
public void setInReplyTo(String inReplyTo) throws MessagingException {
|
|
setHeader("In-Reply-To", inReplyTo);
|
|
}
|
|
|
|
@Override
|
|
public String[] getReferences() throws MessagingException {
|
|
if (mReferences == null) {
|
|
mReferences = getHeader("References");
|
|
}
|
|
return mReferences;
|
|
}
|
|
|
|
@Override
|
|
public void setReferences(String references) throws MessagingException {
|
|
/*
|
|
* Make sure the References header doesn't exceed the maximum header
|
|
* line length and won't get (Q-)encoded later. Otherwise some clients
|
|
* will break threads apart.
|
|
*
|
|
* For more information see issue 1559.
|
|
*/
|
|
|
|
// Make sure separator is SPACE to prevent Q-encoding when TAB is encountered
|
|
references = references.replaceAll("\\s+", " ");
|
|
|
|
/*
|
|
* NOTE: Usually the maximum header line is 998 + CRLF = 1000 characters.
|
|
* But at least one implementations seems to have problems with 998
|
|
* characters, so we adjust for that fact.
|
|
*/
|
|
final int limit = 1000 - 2 /* CRLF */ - 12 /* "References: " */ - 1 /* Off-by-one bugs */;
|
|
final int originalLength = references.length();
|
|
if (originalLength >= limit) {
|
|
// Find start of first reference
|
|
final int start = references.indexOf('<');
|
|
|
|
// First reference + SPACE
|
|
final String firstReference = references.substring(start,
|
|
references.indexOf('<', start + 1));
|
|
|
|
// Find longest tail
|
|
final String tail = references.substring(references.indexOf('<',
|
|
firstReference.length() + originalLength - limit));
|
|
|
|
references = firstReference + tail;
|
|
}
|
|
setHeader("References", references);
|
|
}
|
|
|
|
@Override
|
|
public Body getBody() {
|
|
return mBody;
|
|
}
|
|
|
|
@Override
|
|
public void setBody(Body body) throws MessagingException {
|
|
this.mBody = body;
|
|
setHeader("MIME-Version", "1.0");
|
|
if (body instanceof Multipart) {
|
|
Multipart multipart = ((Multipart)body);
|
|
multipart.setParent(this);
|
|
String type = multipart.getContentType();
|
|
setHeader(MimeHeader.HEADER_CONTENT_TYPE, type);
|
|
if ("multipart/signed".equalsIgnoreCase(type)) {
|
|
setEncoding(MimeUtil.ENC_7BIT);
|
|
} else {
|
|
setEncoding(MimeUtil.ENC_8BIT);
|
|
}
|
|
} else if (body instanceof TextBody) {
|
|
setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\r\n charset=utf-8",
|
|
getMimeType()));
|
|
setEncoding(MimeUtil.ENC_8BIT);
|
|
}
|
|
}
|
|
|
|
protected String getFirstHeader(String name) {
|
|
return mHeader.getFirstHeader(name);
|
|
}
|
|
|
|
@Override
|
|
public void addHeader(String name, String value) throws UnavailableStorageException {
|
|
mHeader.addHeader(name, value);
|
|
}
|
|
|
|
@Override
|
|
public void setHeader(String name, String value) throws UnavailableStorageException {
|
|
mHeader.setHeader(name, value);
|
|
}
|
|
|
|
@Override
|
|
public String[] getHeader(String name) throws UnavailableStorageException {
|
|
return mHeader.getHeader(name);
|
|
}
|
|
|
|
@Override
|
|
public void removeHeader(String name) throws UnavailableStorageException {
|
|
mHeader.removeHeader(name);
|
|
}
|
|
|
|
@Override
|
|
public Set<String> getHeaderNames() throws UnavailableStorageException {
|
|
return mHeader.getHeaderNames();
|
|
}
|
|
|
|
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
|
|
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
|
|
mHeader.writeTo(out);
|
|
writer.write("\r\n");
|
|
writer.flush();
|
|
if (mBody != null) {
|
|
mBody.writeTo(out);
|
|
}
|
|
}
|
|
|
|
public InputStream getInputStream() throws MessagingException {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public void setEncoding(String encoding) throws MessagingException {
|
|
if (mBody != null) {
|
|
mBody.setEncoding(encoding);
|
|
}
|
|
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
|
|
}
|
|
|
|
@Override
|
|
public void setCharset(String charset) throws MessagingException {
|
|
mHeader.setCharset(charset);
|
|
if (mBody instanceof Multipart) {
|
|
((Multipart)mBody).setCharset(charset);
|
|
} else if (mBody instanceof TextBody) {
|
|
MimeUtility.setCharset(charset, this);
|
|
((TextBody)mBody).setCharset(charset);
|
|
}
|
|
}
|
|
|
|
class MimeMessageBuilder implements ContentHandler {
|
|
private final LinkedList<Object> stack = new LinkedList<Object>();
|
|
|
|
public MimeMessageBuilder() {
|
|
}
|
|
|
|
private void expect(Class<?> c) {
|
|
if (!c.isInstance(stack.peek())) {
|
|
throw new IllegalStateException("Internal stack error: " + "Expected '"
|
|
+ c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
|
|
}
|
|
}
|
|
|
|
public void startMessage() {
|
|
if (stack.isEmpty()) {
|
|
stack.addFirst(MimeMessage.this);
|
|
} else {
|
|
expect(Part.class);
|
|
try {
|
|
MimeMessage m = new MimeMessage();
|
|
((Part)stack.peek()).setBody(m);
|
|
stack.addFirst(m);
|
|
} catch (MessagingException me) {
|
|
throw new Error(me);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void endMessage() {
|
|
expect(MimeMessage.class);
|
|
stack.removeFirst();
|
|
}
|
|
|
|
public void startHeader() {
|
|
expect(Part.class);
|
|
}
|
|
|
|
|
|
|
|
|
|
public void endHeader() {
|
|
expect(Part.class);
|
|
}
|
|
|
|
public void startMultipart(BodyDescriptor bd) {
|
|
expect(Part.class);
|
|
|
|
Part e = (Part)stack.peek();
|
|
try {
|
|
MimeMultipart multiPart = new MimeMultipart(e.getContentType());
|
|
e.setBody(multiPart);
|
|
stack.addFirst(multiPart);
|
|
} catch (MessagingException me) {
|
|
throw new Error(me);
|
|
}
|
|
}
|
|
|
|
public void body(BodyDescriptor bd, InputStream in) throws IOException {
|
|
expect(Part.class);
|
|
try {
|
|
Body body = MimeUtility.decodeBody(in,
|
|
bd.getTransferEncoding(), bd.getMimeType());
|
|
((Part)stack.peek()).setBody(body);
|
|
} catch (MessagingException me) {
|
|
throw new Error(me);
|
|
}
|
|
}
|
|
|
|
public void endMultipart() {
|
|
stack.removeFirst();
|
|
}
|
|
|
|
public void startBodyPart() {
|
|
expect(MimeMultipart.class);
|
|
|
|
try {
|
|
MimeBodyPart bodyPart = new MimeBodyPart();
|
|
((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
|
|
stack.addFirst(bodyPart);
|
|
} catch (MessagingException me) {
|
|
throw new Error(me);
|
|
}
|
|
}
|
|
|
|
public void endBodyPart() {
|
|
expect(BodyPart.class);
|
|
stack.removeFirst();
|
|
}
|
|
|
|
public void epilogue(InputStream is) throws IOException {
|
|
expect(MimeMultipart.class);
|
|
StringBuilder sb = new StringBuilder();
|
|
int b;
|
|
while ((b = is.read()) != -1) {
|
|
sb.append((char)b);
|
|
}
|
|
// ((Multipart) stack.peek()).setEpilogue(sb.toString());
|
|
}
|
|
|
|
public void preamble(InputStream is) throws IOException {
|
|
expect(MimeMultipart.class);
|
|
StringBuilder sb = new StringBuilder();
|
|
int b;
|
|
while ((b = is.read()) != -1) {
|
|
sb.append((char)b);
|
|
}
|
|
((MimeMultipart)stack.peek()).setPreamble(sb.toString());
|
|
|
|
}
|
|
|
|
public void raw(InputStream is) throws IOException {
|
|
throw new UnsupportedOperationException("Not supported");
|
|
}
|
|
|
|
@Override
|
|
public void field(Field parsedField) throws MimeException {
|
|
expect(Part.class);
|
|
try {
|
|
((Part)stack.peek()).addHeader(parsedField.getName(), parsedField.getBody().trim());
|
|
} catch (MessagingException me) {
|
|
throw new Error(me);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy the contents of this object into another {@code MimeMessage} object.
|
|
*
|
|
* @param message
|
|
* The {@code MimeMessage} object to receive the contents of this instance.
|
|
*/
|
|
protected void copy(MimeMessage message) {
|
|
super.copy(message);
|
|
|
|
message.mHeader = mHeader.clone();
|
|
|
|
message.mBody = mBody;
|
|
message.mMessageId = mMessageId;
|
|
message.mSentDate = mSentDate;
|
|
message.mDateFormat = mDateFormat;
|
|
message.mSize = mSize;
|
|
|
|
// These arrays are not supposed to be modified, so it's okay to reuse the references
|
|
message.mFrom = mFrom;
|
|
message.mTo = mTo;
|
|
message.mCc = mCc;
|
|
message.mBcc = mBcc;
|
|
message.mReplyTo = mReplyTo;
|
|
message.mReferences = mReferences;
|
|
message.mInReplyTo = mInReplyTo;
|
|
}
|
|
|
|
@Override
|
|
public MimeMessage clone() {
|
|
MimeMessage message = new MimeMessage();
|
|
copy(message);
|
|
return message;
|
|
}
|
|
|
|
public long getId() {
|
|
return Long.parseLong(mUid); //or maybe .mMessageId?
|
|
}
|
|
|
|
public String getPreview() {
|
|
return "";
|
|
}
|
|
|
|
public boolean hasAttachments() {
|
|
return false;
|
|
}
|
|
|
|
|
|
@Override
|
|
public void setUsing7bitTransport() throws MessagingException {
|
|
String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
|
|
/*
|
|
* We don't trust that a multipart/* will properly have an 8bit encoding
|
|
* header if any of its subparts are 8bit, so we automatically recurse
|
|
* (as long as its not multipart/signed).
|
|
*/
|
|
if (mBody instanceof CompositeBody
|
|
&& !"multipart/signed".equalsIgnoreCase(type)) {
|
|
setEncoding(MimeUtil.ENC_7BIT);
|
|
// recurse
|
|
((CompositeBody) mBody).setUsing7bitTransport();
|
|
} else if (!MimeUtil.ENC_8BIT
|
|
.equalsIgnoreCase(getFirstHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING))) {
|
|
return;
|
|
} else if (type != null
|
|
&& (type.equalsIgnoreCase("multipart/signed") || type
|
|
.toLowerCase(Locale.US).startsWith("message/"))) {
|
|
/*
|
|
* This shouldn't happen. In any case, it would be wrong to convert
|
|
* them to some other encoding for 7bit transport.
|
|
*
|
|
* RFC 1847 says multipart/signed must be 7bit. It also says their
|
|
* bodies must be treated as opaque, so we must not change the
|
|
* encoding.
|
|
*
|
|
* We've dealt with (CompositeBody) type message/rfc822 above. Here
|
|
* we must deal with all other message/* types. RFC 2045 says
|
|
* message/* can only be 7bit or 8bit. RFC 2046 says unknown
|
|
* message/* types must be treated as application/octet-stream,
|
|
* which means we can't recurse into them. It also says that
|
|
* existing subtypes message/partial and message/external must only
|
|
* be 7bit, and that future subtypes "should be" 7bit.
|
|
*/
|
|
throw new MessagingException(
|
|
"Unable to convert 8bit body part to 7bit");
|
|
} else {
|
|
setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
|
|
}
|
|
}
|
|
}
|