mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-17 06:55:03 -05:00
Merge branch 'viewables'
This commit is contained in:
commit
8e32320f5d
@ -2709,7 +2709,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc
|
|||||||
if (part != null) {
|
if (part != null) {
|
||||||
if (K9.DEBUG)
|
if (K9.DEBUG)
|
||||||
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, text found.");
|
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, text found.");
|
||||||
return HtmlConverter.textToHtml(MimeUtility.getTextFromPart(part));
|
return HtmlConverter.textToHtml(MimeUtility.getTextFromPart(part), true);
|
||||||
}
|
}
|
||||||
} else if (format == MessageFormat.TEXT) {
|
} else if (format == MessageFormat.TEXT) {
|
||||||
// Text takes precedence, then html.
|
// Text takes precedence, then html.
|
||||||
|
@ -1691,9 +1691,7 @@ public class MessagingController implements Runnable {
|
|||||||
* right now, attachments will be left for later.
|
* right now, attachments will be left for later.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ArrayList<Part> viewables = new ArrayList<Part>();
|
Set<Part> viewables = MimeUtility.collectTextParts(message);
|
||||||
ArrayList<Part> attachments = new ArrayList<Part>();
|
|
||||||
MimeUtility.collectParts(message, viewables, attachments);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Now download the parts we're interested in storing.
|
* Now download the parts we're interested in storing.
|
||||||
@ -2816,9 +2814,7 @@ public class MessagingController implements Runnable {
|
|||||||
try {
|
try {
|
||||||
LocalStore localStore = account.getLocalStore();
|
LocalStore localStore = account.getLocalStore();
|
||||||
|
|
||||||
ArrayList<Part> viewables = new ArrayList<Part>();
|
List<Part> attachments = MimeUtility.collectAttachments(message);
|
||||||
ArrayList<Part> attachments = new ArrayList<Part>();
|
|
||||||
MimeUtility.collectParts(message, viewables, attachments);
|
|
||||||
for (Part attachment : attachments) {
|
for (Part attachment : attachments) {
|
||||||
attachment.setBody(null);
|
attachment.setBody(null);
|
||||||
}
|
}
|
||||||
|
@ -125,19 +125,41 @@ public class HtmlConverter {
|
|||||||
|
|
||||||
private static final int MAX_SMART_HTMLIFY_MESSAGE_LENGTH = 1024 * 256 ;
|
private static final int MAX_SMART_HTMLIFY_MESSAGE_LENGTH = 1024 * 256 ;
|
||||||
|
|
||||||
|
public static final String getHtmlHeader() {
|
||||||
|
return "<html><head/><body>";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String getHtmlFooter() {
|
||||||
|
return "</body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Naively convert a text string into an HTML document. This method avoids using regular expressions on the entire
|
* Naively convert a text string into an HTML document.
|
||||||
* message body to save memory.
|
*
|
||||||
* @param text Plain text string.
|
* <p>
|
||||||
|
* This method avoids using regular expressions on the entire message body to save memory.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
* Plain text string.
|
||||||
|
* @param useHtmlTag
|
||||||
|
* If {@code true} this method adds headers and footers to create a proper HTML
|
||||||
|
* document.
|
||||||
|
*
|
||||||
* @return HTML string.
|
* @return HTML string.
|
||||||
*/
|
*/
|
||||||
private static String simpleTextToHtml(String text) {
|
private static String simpleTextToHtml(String text, boolean useHtmlTag) {
|
||||||
// Encode HTML entities to make sure we don't display something evil.
|
// Encode HTML entities to make sure we don't display something evil.
|
||||||
text = TextUtils.htmlEncode(text);
|
text = TextUtils.htmlEncode(text);
|
||||||
|
|
||||||
StringReader reader = new StringReader(text);
|
StringReader reader = new StringReader(text);
|
||||||
StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
|
StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
|
||||||
buff.append("<html><head/><body>");
|
|
||||||
|
if (useHtmlTag) {
|
||||||
|
buff.append(getHtmlHeader());
|
||||||
|
}
|
||||||
|
|
||||||
|
buff.append(htmlifyMessageHeader());
|
||||||
|
|
||||||
int c;
|
int c;
|
||||||
try {
|
try {
|
||||||
@ -159,25 +181,39 @@ public class HtmlConverter {
|
|||||||
Log.e(K9.LOG_TAG, "Could not read string to convert text to HTML:", e);
|
Log.e(K9.LOG_TAG, "Could not read string to convert text to HTML:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
buff.append("</body></html>");
|
buff.append(htmlifyMessageFooter());
|
||||||
|
|
||||||
|
if (useHtmlTag) {
|
||||||
|
buff.append(getHtmlFooter());
|
||||||
|
}
|
||||||
|
|
||||||
return buff.toString();
|
return buff.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a text string into an HTML document. Attempts to do smart replacement for large
|
* Convert a text string into an HTML document.
|
||||||
* documents to prevent OOM errors. This method adds headers and footers to create a proper HTML
|
*
|
||||||
* document. To convert to a fragment, use {@link #textToHtmlFragment(String)}.
|
* <p>
|
||||||
* @param text Plain text string.
|
* Attempts to do smart replacement for large documents to prevent OOM errors. This method
|
||||||
|
* optionally adds headers and footers to create a proper HTML document. To convert to a
|
||||||
|
* fragment, use {@link #textToHtmlFragment(String)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
* Plain text string.
|
||||||
|
* @param useHtmlTag
|
||||||
|
* If {@code true} this method adds headers and footers to create a proper HTML
|
||||||
|
* document.
|
||||||
|
*
|
||||||
* @return HTML string.
|
* @return HTML string.
|
||||||
*/
|
*/
|
||||||
public static String textToHtml(String text) {
|
public static String textToHtml(String text, boolean useHtmlTag) {
|
||||||
// Our HTMLification code is somewhat memory intensive
|
// Our HTMLification code is somewhat memory intensive
|
||||||
// and was causing lots of OOM errors on the market
|
// and was causing lots of OOM errors on the market
|
||||||
// if the message is big and plain text, just do
|
// if the message is big and plain text, just do
|
||||||
// a trivial htmlification
|
// a trivial htmlification
|
||||||
if (text.length() > MAX_SMART_HTMLIFY_MESSAGE_LENGTH) {
|
if (text.length() > MAX_SMART_HTMLIFY_MESSAGE_LENGTH) {
|
||||||
return simpleTextToHtml(text);
|
return simpleTextToHtml(text, useHtmlTag);
|
||||||
}
|
}
|
||||||
StringReader reader = new StringReader(text);
|
StringReader reader = new StringReader(text);
|
||||||
StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
|
StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
|
||||||
@ -221,11 +257,19 @@ public class HtmlConverter {
|
|||||||
text = text.replaceAll("(?m)(\r\n|\n|\r){4,}", "\n\n");
|
text = text.replaceAll("(?m)(\r\n|\n|\r){4,}", "\n\n");
|
||||||
|
|
||||||
StringBuffer sb = new StringBuffer(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
|
StringBuffer sb = new StringBuffer(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
|
||||||
sb.append("<html><head></head><body>");
|
|
||||||
|
if (useHtmlTag) {
|
||||||
|
sb.append(getHtmlHeader());
|
||||||
|
}
|
||||||
|
|
||||||
sb.append(htmlifyMessageHeader());
|
sb.append(htmlifyMessageHeader());
|
||||||
linkifyText(text, sb);
|
linkifyText(text, sb);
|
||||||
sb.append(htmlifyMessageFooter());
|
sb.append(htmlifyMessageFooter());
|
||||||
sb.append("</body></html>");
|
|
||||||
|
if (useHtmlTag) {
|
||||||
|
sb.append(getHtmlFooter());
|
||||||
|
}
|
||||||
|
|
||||||
text = sb.toString();
|
text = sb.toString();
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.internet;
|
package com.fsck.k9.mail.internet;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.fsck.k9.K9;
|
import com.fsck.k9.K9;
|
||||||
|
import com.fsck.k9.R;
|
||||||
|
import com.fsck.k9.helper.HtmlConverter;
|
||||||
import com.fsck.k9.mail.*;
|
import com.fsck.k9.mail.*;
|
||||||
|
import com.fsck.k9.mail.Message.RecipientType;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.james.mime4j.codec.Base64InputStream;
|
import org.apache.james.mime4j.codec.Base64InputStream;
|
||||||
import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
|
import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
|
||||||
@ -12,7 +17,11 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.IllegalCharsetNameException;
|
import java.nio.charset.IllegalCharsetNameException;
|
||||||
@ -23,6 +32,9 @@ public class MimeUtility {
|
|||||||
|
|
||||||
public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings";
|
public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings";
|
||||||
|
|
||||||
|
private static final String TEXT_DIVIDER =
|
||||||
|
"------------------------------------------------------------------------";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* http://www.w3schools.com/media/media_mimeref.asp
|
* http://www.w3schools.com/media/media_mimeref.asp
|
||||||
* +
|
* +
|
||||||
@ -1100,49 +1112,867 @@ public class MimeUtility {
|
|||||||
return tempBody;
|
return tempBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An unfortunately named method that makes decisions about a Part (usually a Message)
|
* Empty base class for the class hierarchy used by
|
||||||
* as to which of it's children will be "viewable" and which will be attachments.
|
* {@link MimeUtility#extractTextAndAttachments(Context, Message)}.
|
||||||
* The method recursively sorts the viewables and attachments into seperate
|
*
|
||||||
* lists for further processing.
|
* @see Text
|
||||||
* @param part
|
* @see Html
|
||||||
* @param viewables
|
* @see MessageHeader
|
||||||
* @param attachments
|
* @see Alternative
|
||||||
* @throws MessagingException
|
|
||||||
*/
|
*/
|
||||||
public static void collectParts(Part part, ArrayList<Part> viewables,
|
static abstract class Viewable { /* empty */ }
|
||||||
ArrayList<Part> attachments) throws MessagingException {
|
|
||||||
/*
|
/**
|
||||||
* If the part is Multipart but not alternative it's either mixed or
|
* Class representing textual parts of a message that aren't marked as attachments.
|
||||||
* something we don't know about, which means we treat it as mixed
|
*
|
||||||
* per the spec. We just process it's pieces recursively.
|
* @see MimeUtility#isPartTextualBody(Part)
|
||||||
|
*/
|
||||||
|
static abstract class Textual extends Viewable {
|
||||||
|
private Part mPart;
|
||||||
|
|
||||||
|
public Textual(Part part) {
|
||||||
|
mPart = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Part getPart() {
|
||||||
|
return mPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a {@code text/plain} part of a message.
|
||||||
|
*/
|
||||||
|
static class Text extends Textual {
|
||||||
|
public Text(Part part) {
|
||||||
|
super(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a {@code text/html} part of a message.
|
||||||
|
*/
|
||||||
|
static class Html extends Textual {
|
||||||
|
public Html(Part part) {
|
||||||
|
super(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a {@code message/rfc822} part of a message.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This is used to extract basic header information when the message contents are displayed
|
||||||
|
* inline.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
static class MessageHeader extends Viewable {
|
||||||
|
private Part mContainerPart;
|
||||||
|
private Message mMessage;
|
||||||
|
|
||||||
|
public MessageHeader(Part containerPart, Message message) {
|
||||||
|
mContainerPart = containerPart;
|
||||||
|
mMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Part getContainerPart() {
|
||||||
|
return mContainerPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message getMessage() {
|
||||||
|
return mMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a {@code multipart/alternative} part of a message.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Only relevant {@code text/plain} and {@code text/html} children are stored in this container
|
||||||
|
* class.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
static class Alternative extends Viewable {
|
||||||
|
private List<Viewable> mText;
|
||||||
|
private List<Viewable> mHtml;
|
||||||
|
|
||||||
|
public Alternative(List<Viewable> text, List<Viewable> html) {
|
||||||
|
mText = text;
|
||||||
|
mHtml = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Viewable> getText() {
|
||||||
|
return mText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Viewable> getHtml() {
|
||||||
|
return mHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store viewable text of a message as plain text and HTML, and the parts considered
|
||||||
|
* attachments.
|
||||||
|
*
|
||||||
|
* @see MimeUtility#extractTextAndAttachments(Context, Message)
|
||||||
|
*/
|
||||||
|
public static class ViewableContainer {
|
||||||
|
/**
|
||||||
|
* The viewable text of the message in plain text.
|
||||||
*/
|
*/
|
||||||
if (part.getBody() instanceof Multipart) {
|
public final String text;
|
||||||
Multipart mp = (Multipart)part.getBody();
|
|
||||||
for (int i = 0; i < mp.getCount(); i++) {
|
/**
|
||||||
collectParts(mp.getBodyPart(i), viewables, attachments);
|
* The viewable text of the message in HTML.
|
||||||
|
*/
|
||||||
|
public final String html;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parts of the message considered attachments (everything not viewable).
|
||||||
|
*/
|
||||||
|
public final List<Part> attachments;
|
||||||
|
|
||||||
|
ViewableContainer(String text, String html, List<Part> attachments) {
|
||||||
|
this.text = text;
|
||||||
|
this.html = html;
|
||||||
|
this.attachments = attachments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect attachment parts of a message.
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
* The message to collect the attachment parts from.
|
||||||
|
*
|
||||||
|
* @return A list of parts regarded as attachments.
|
||||||
|
*
|
||||||
|
* @throws MessagingException
|
||||||
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
public static List<Part> collectAttachments(Message message)
|
||||||
|
throws MessagingException {
|
||||||
|
try {
|
||||||
|
List<Part> attachments = new ArrayList<Part>();
|
||||||
|
getViewables(message, attachments);
|
||||||
|
|
||||||
|
return attachments;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MessagingException("Couldn't collect attachment parts", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect the viewable textual parts of a message.
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
* The message to extract the viewable parts from.
|
||||||
|
*
|
||||||
|
* @return A set of viewable parts of the message.
|
||||||
|
*
|
||||||
|
* @throws MessagingException
|
||||||
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
public static Set<Part> collectTextParts(Message message)
|
||||||
|
throws MessagingException {
|
||||||
|
try {
|
||||||
|
List<Part> attachments = new ArrayList<Part>();
|
||||||
|
|
||||||
|
// Collect all viewable parts
|
||||||
|
List<Viewable> viewables = getViewables(message, attachments);
|
||||||
|
|
||||||
|
// Extract the Part references
|
||||||
|
return getParts(viewables);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MessagingException("Couldn't extract viewable parts", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the viewable textual parts of a message and return the rest as attachments.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* A {@link Context} instance that will be used to get localized strings.
|
||||||
|
* @param message
|
||||||
|
* The message to extract the text and attachments from.
|
||||||
|
*
|
||||||
|
* @return A {@link ViewableContainer} instance containing the textual parts of the message as
|
||||||
|
* plain text and HTML, and a list of message parts considered attachments.
|
||||||
|
*
|
||||||
|
* @throws MessagingException
|
||||||
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
public static ViewableContainer extractTextAndAttachments(Context context, Message message)
|
||||||
|
throws MessagingException {
|
||||||
|
try {
|
||||||
|
List<Part> attachments = new ArrayList<Part>();
|
||||||
|
|
||||||
|
// Collect all viewable parts
|
||||||
|
List<Viewable> viewables = getViewables(message, attachments);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert the tree of viewable parts into text and HTML
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Used to suppress the divider for the first viewable part
|
||||||
|
boolean hideDivider = true;
|
||||||
|
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
html.append(HtmlConverter.getHtmlHeader());
|
||||||
|
|
||||||
|
for (Viewable viewable : viewables) {
|
||||||
|
if (viewable instanceof Textual) {
|
||||||
|
// This is either a text/plain or text/html part. Fill the variables 'text' and
|
||||||
|
// 'html', converting between plain text and HTML as necessary.
|
||||||
|
text.append(buildText(viewable, !hideDivider));
|
||||||
|
html.append(buildHtml(viewable, !hideDivider));
|
||||||
|
hideDivider = false;
|
||||||
|
} else if (viewable instanceof MessageHeader) {
|
||||||
|
MessageHeader header = (MessageHeader) viewable;
|
||||||
|
Part containerPart = header.getContainerPart();
|
||||||
|
Message innerMessage = header.getMessage();
|
||||||
|
|
||||||
|
addTextDivider(text, containerPart, !hideDivider);
|
||||||
|
addMessageHeaderText(context, text, innerMessage);
|
||||||
|
|
||||||
|
addHtmlDivider(html, containerPart, !hideDivider);
|
||||||
|
addMessageHeaderHtml(context, html, innerMessage);
|
||||||
|
|
||||||
|
hideDivider = true;
|
||||||
|
} else if (viewable instanceof Alternative) {
|
||||||
|
// Handle multipart/alternative contents
|
||||||
|
Alternative alternative = (Alternative) viewable;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We made sure at least one of text/plain or text/html is present when
|
||||||
|
* creating the Alternative object. If one part is not present we convert the
|
||||||
|
* other one to make sure 'text' and 'html' always contain the same text.
|
||||||
|
*/
|
||||||
|
List<Viewable> textAlternative = alternative.getText().isEmpty() ?
|
||||||
|
alternative.getHtml() : alternative.getText();
|
||||||
|
List<Viewable> htmlAlternative = alternative.getHtml().isEmpty() ?
|
||||||
|
alternative.getText() : alternative.getHtml();
|
||||||
|
|
||||||
|
// Fill the 'text' variable
|
||||||
|
boolean divider = !hideDivider;
|
||||||
|
for (Viewable textViewable : textAlternative) {
|
||||||
|
text.append(buildText(textViewable, divider));
|
||||||
|
divider = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the 'html' variable
|
||||||
|
divider = !hideDivider;
|
||||||
|
for (Viewable htmlViewable : htmlAlternative) {
|
||||||
|
html.append(buildHtml(htmlViewable, divider));
|
||||||
|
divider = true;
|
||||||
|
}
|
||||||
|
hideDivider = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.append(HtmlConverter.getHtmlFooter());
|
||||||
|
|
||||||
|
return new ViewableContainer(text.toString(), html.toString(), attachments);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MessagingException("Couldn't extract viewable parts", e);
|
||||||
}
|
}
|
||||||
/*
|
}
|
||||||
* If the part is an embedded message we just continue to process
|
|
||||||
* it, pulling any viewables or attachments into the running list.
|
/**
|
||||||
*/
|
* Traverse the MIME tree of a message an extract viewable parts.
|
||||||
else if (part.getBody() instanceof Message) {
|
*
|
||||||
Message message = (Message)part.getBody();
|
* @param part
|
||||||
collectParts(message, viewables, attachments);
|
* The message part to start from.
|
||||||
}
|
* @param attachments
|
||||||
/*
|
* A list that will receive the parts that are considered attachments.
|
||||||
* If the part is HTML and it got this far it's part of a mixed (et
|
*
|
||||||
* al) and should be rendered inline.
|
* @return A list of {@link Viewable}s.
|
||||||
*/
|
*
|
||||||
else if (isPartTextualBody(part)) {
|
* @throws MessagingException
|
||||||
viewables.add(part);
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
public static List<Viewable> getViewables(Part part, List<Part> attachments) throws MessagingException {
|
||||||
|
List<Viewable> viewables = new ArrayList<Viewable>();
|
||||||
|
|
||||||
|
Body body = part.getBody();
|
||||||
|
if (body instanceof Multipart) {
|
||||||
|
Multipart multipart = (Multipart) body;
|
||||||
|
if (part.getMimeType().equalsIgnoreCase("multipart/alternative")) {
|
||||||
|
/*
|
||||||
|
* For multipart/alternative parts we try to find a text/plain and a text/html
|
||||||
|
* child. Everything else we find is put into 'attachments'.
|
||||||
|
*/
|
||||||
|
List<Viewable> text = findTextPart(multipart, true);
|
||||||
|
|
||||||
|
Set<Part> knownTextParts = getParts(text);
|
||||||
|
List<Viewable> html = findHtmlPart(multipart, knownTextParts, attachments, true);
|
||||||
|
|
||||||
|
if (!text.isEmpty() || !html.isEmpty()) {
|
||||||
|
Alternative alternative = new Alternative(text, html);
|
||||||
|
viewables.add(alternative);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For all other multipart parts we recurse to grab all viewable children.
|
||||||
|
int childCount = multipart.getCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
Part bodyPart = multipart.getBodyPart(i);
|
||||||
|
viewables.addAll(getViewables(bodyPart, attachments));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (body instanceof Message &&
|
||||||
|
!("attachment".equalsIgnoreCase(getContentDisposition(part)))) {
|
||||||
|
/*
|
||||||
|
* We only care about message/rfc822 parts whose Content-Disposition header has a value
|
||||||
|
* other than "attachment".
|
||||||
|
*/
|
||||||
|
Message message = (Message) body;
|
||||||
|
|
||||||
|
// We add the Message object so we can extract the filename later.
|
||||||
|
viewables.add(new MessageHeader(part, message));
|
||||||
|
|
||||||
|
// Recurse to grab all viewable parts and attachments from that message.
|
||||||
|
viewables.addAll(getViewables(message, attachments));
|
||||||
|
} else if (isPartTextualBody(part)) {
|
||||||
|
/*
|
||||||
|
* Save text/plain and text/html
|
||||||
|
*/
|
||||||
|
String mimeType = part.getMimeType();
|
||||||
|
if (mimeType.equalsIgnoreCase("text/plain")) {
|
||||||
|
Text text = new Text(part);
|
||||||
|
viewables.add(text);
|
||||||
|
} else {
|
||||||
|
Html html = new Html(part);
|
||||||
|
viewables.add(html);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Everything else is treated as attachment.
|
||||||
attachments.add(part);
|
attachments.add(part);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return viewables;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the children of a {@link Multipart} for {@code text/plain} parts.
|
||||||
|
*
|
||||||
|
* @param multipart
|
||||||
|
* The {@code Multipart} to search through.
|
||||||
|
* @param directChild
|
||||||
|
* If {@code true}, this method will return after the first {@code text/plain} was
|
||||||
|
* found.
|
||||||
|
*
|
||||||
|
* @return A list of {@link Text} viewables.
|
||||||
|
*
|
||||||
|
* @throws MessagingException
|
||||||
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
private static List<Viewable> findTextPart(Multipart multipart, boolean directChild)
|
||||||
|
throws MessagingException {
|
||||||
|
List<Viewable> viewables = new ArrayList<Viewable>();
|
||||||
|
|
||||||
|
int childCount = multipart.getCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
Part part = multipart.getBodyPart(i);
|
||||||
|
Body body = part.getBody();
|
||||||
|
if (body instanceof Multipart) {
|
||||||
|
Multipart innerMultipart = (Multipart) body;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Recurse to find text parts. Since this is a multipart that is a child of a
|
||||||
|
* multipart/alternative we don't want to stop after the first text/plain part
|
||||||
|
* we find. This will allow to get all text parts for constructions like this:
|
||||||
|
*
|
||||||
|
* 1. multipart/alternative
|
||||||
|
* 1.1. multipart/mixed
|
||||||
|
* 1.1.1. text/plain
|
||||||
|
* 1.1.2. text/plain
|
||||||
|
* 1.2. text/html
|
||||||
|
*/
|
||||||
|
List<Viewable> textViewables = findTextPart(innerMultipart, false);
|
||||||
|
|
||||||
|
if (!textViewables.isEmpty()) {
|
||||||
|
viewables.addAll(textViewables);
|
||||||
|
if (directChild) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isPartTextualBody(part) && part.getMimeType().equalsIgnoreCase("text/plain")) {
|
||||||
|
Text text = new Text(part);
|
||||||
|
viewables.add(text);
|
||||||
|
if (directChild) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the children of a {@link Multipart} for {@code text/html} parts.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Every part that is not a {@code text/html} we want to display, we add to 'attachments'.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param multipart
|
||||||
|
* The {@code Multipart} to search through.
|
||||||
|
* @param knownTextParts
|
||||||
|
* A set of {@code text/plain} parts that shouldn't be added to 'attachments'.
|
||||||
|
* @param attachments
|
||||||
|
* A list that will receive the parts that are considered attachments.
|
||||||
|
* @param directChild
|
||||||
|
* If {@code true}, this method will add all {@code text/html} parts except the first
|
||||||
|
* found to 'attachments'.
|
||||||
|
*
|
||||||
|
* @return A list of {@link Text} viewables.
|
||||||
|
*
|
||||||
|
* @throws MessagingException
|
||||||
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
private static List<Viewable> findHtmlPart(Multipart multipart, Set<Part> knownTextParts,
|
||||||
|
List<Part> attachments, boolean directChild) throws MessagingException {
|
||||||
|
List<Viewable> viewables = new ArrayList<Viewable>();
|
||||||
|
|
||||||
|
boolean partFound = false;
|
||||||
|
int childCount = multipart.getCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
Part part = multipart.getBodyPart(i);
|
||||||
|
Body body = part.getBody();
|
||||||
|
if (body instanceof Multipart) {
|
||||||
|
Multipart innerMultipart = (Multipart) body;
|
||||||
|
|
||||||
|
if (directChild && partFound) {
|
||||||
|
// We already found our text/html part. Now we're only looking for attachments.
|
||||||
|
findAttachments(innerMultipart, knownTextParts, attachments);
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* Recurse to find HTML parts. Since this is a multipart that is a child of a
|
||||||
|
* multipart/alternative we don't want to stop after the first text/html part
|
||||||
|
* we find. This will allow to get all text parts for constructions like this:
|
||||||
|
*
|
||||||
|
* 1. multipart/alternative
|
||||||
|
* 1.1. text/plain
|
||||||
|
* 1.2. multipart/mixed
|
||||||
|
* 1.2.1. text/html
|
||||||
|
* 1.2.2. text/html
|
||||||
|
* 1.3. image/jpeg
|
||||||
|
*/
|
||||||
|
List<Viewable> htmlViewables = findHtmlPart(innerMultipart, knownTextParts,
|
||||||
|
attachments, false);
|
||||||
|
|
||||||
|
if (!htmlViewables.isEmpty()) {
|
||||||
|
partFound = true;
|
||||||
|
viewables.addAll(htmlViewables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!(directChild && partFound) && isPartTextualBody(part) &&
|
||||||
|
part.getMimeType().equalsIgnoreCase("text/html")) {
|
||||||
|
Html html = new Html(part);
|
||||||
|
viewables.add(html);
|
||||||
|
partFound = true;
|
||||||
|
} else if (!knownTextParts.contains(part)) {
|
||||||
|
// Only add this part as attachment if it's not a viewable text/plain part found
|
||||||
|
// earlier.
|
||||||
|
attachments.add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a set of message parts for fast lookups.
|
||||||
|
*
|
||||||
|
* @param viewables
|
||||||
|
* A list of {@link Viewable}s containing references to the message parts to include in
|
||||||
|
* the set.
|
||||||
|
*
|
||||||
|
* @return The set of viewable {@code Part}s.
|
||||||
|
*
|
||||||
|
* @see MimeUtility#findHtmlPart(Multipart, Set, List, boolean)
|
||||||
|
* @see MimeUtility#findAttachments(Multipart, Set, List)
|
||||||
|
*/
|
||||||
|
private static Set<Part> getParts(List<Viewable> viewables) {
|
||||||
|
Set<Part> parts = new HashSet<Part>();
|
||||||
|
|
||||||
|
for (Viewable viewable : viewables) {
|
||||||
|
if (viewable instanceof Textual) {
|
||||||
|
parts.add(((Textual) viewable).getPart());
|
||||||
|
} else if (viewable instanceof Alternative) {
|
||||||
|
Alternative alternative = (Alternative) viewable;
|
||||||
|
parts.addAll(getParts(alternative.getText()));
|
||||||
|
parts.addAll(getParts(alternative.getHtml()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverse the MIME tree and add everything that's not a known text part to 'attachments'.
|
||||||
|
*
|
||||||
|
* @param multipart
|
||||||
|
* The {@link Multipart} to start from.
|
||||||
|
* @param knownTextParts
|
||||||
|
* A set of known text parts we don't want to end up in 'attachments'.
|
||||||
|
* @param attachments
|
||||||
|
* A list that will receive the parts that are considered attachments.
|
||||||
|
*/
|
||||||
|
private static void findAttachments(Multipart multipart, Set<Part> knownTextParts,
|
||||||
|
List<Part> attachments) {
|
||||||
|
int childCount = multipart.getCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
Part part = multipart.getBodyPart(i);
|
||||||
|
Body body = part.getBody();
|
||||||
|
if (body instanceof Multipart) {
|
||||||
|
Multipart innerMultipart = (Multipart) body;
|
||||||
|
findAttachments(innerMultipart, knownTextParts, attachments);
|
||||||
|
} else if (!knownTextParts.contains(part)) {
|
||||||
|
attachments.add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract important header values from a message to display inline (plain text version).
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* A {@link Context} instance that will be used to get localized strings.
|
||||||
|
* @param text
|
||||||
|
* The {@link StringBuilder} that will receive the (plain text) output.
|
||||||
|
* @param message
|
||||||
|
* The message to extract the header values from.
|
||||||
|
*
|
||||||
|
* @throws MessagingException
|
||||||
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
private static void addMessageHeaderText(Context context, StringBuilder text, Message message)
|
||||||
|
throws MessagingException {
|
||||||
|
// From: <sender>
|
||||||
|
Address[] from = message.getFrom();
|
||||||
|
if (from != null && from.length > 0) {
|
||||||
|
text.append(context.getString(R.string.message_compose_quote_header_from));
|
||||||
|
text.append(' ');
|
||||||
|
text.append(Address.toString(from));
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// To: <recipients>
|
||||||
|
Address[] to = message.getRecipients(RecipientType.TO);
|
||||||
|
if (to != null && to.length > 0) {
|
||||||
|
text.append(context.getString(R.string.message_compose_quote_header_to));
|
||||||
|
text.append(' ');
|
||||||
|
text.append(Address.toString(to));
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cc: <recipients>
|
||||||
|
Address[] cc = message.getRecipients(RecipientType.CC);
|
||||||
|
if (cc != null && cc.length > 0) {
|
||||||
|
text.append(context.getString(R.string.message_compose_quote_header_cc));
|
||||||
|
text.append(' ');
|
||||||
|
text.append(Address.toString(cc));
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date: <date>
|
||||||
|
Date date = message.getSentDate();
|
||||||
|
if (date != null) {
|
||||||
|
text.append(context.getString(R.string.message_compose_quote_header_send_date));
|
||||||
|
text.append(' ');
|
||||||
|
text.append(date.toString());
|
||||||
|
text.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject: <subject>
|
||||||
|
String subject = message.getSubject();
|
||||||
|
text.append(context.getString(R.string.message_compose_quote_header_subject));
|
||||||
|
text.append(' ');
|
||||||
|
if (subject == null) {
|
||||||
|
text.append(context.getString(R.string.general_no_subject));
|
||||||
|
} else {
|
||||||
|
text.append(subject);
|
||||||
|
}
|
||||||
|
text.append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract important header values from a message to display inline (HTML version).
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* A {@link Context} instance that will be used to get localized strings.
|
||||||
|
* @param html
|
||||||
|
* The {@link StringBuilder} that will receive the (HTML) output.
|
||||||
|
* @param message
|
||||||
|
* The message to extract the header values from.
|
||||||
|
*
|
||||||
|
* @throws MessagingException
|
||||||
|
* In case of an error.
|
||||||
|
*/
|
||||||
|
private static void addMessageHeaderHtml(Context context, StringBuilder html, Message message)
|
||||||
|
throws MessagingException {
|
||||||
|
|
||||||
|
html.append("<table style=\"border: 0\">");
|
||||||
|
|
||||||
|
// From: <sender>
|
||||||
|
Address[] from = message.getFrom();
|
||||||
|
if (from != null && from.length > 0) {
|
||||||
|
addTableRow(html, context.getString(R.string.message_compose_quote_header_from),
|
||||||
|
Address.toString(from));
|
||||||
|
}
|
||||||
|
|
||||||
|
// To: <recipients>
|
||||||
|
Address[] to = message.getRecipients(RecipientType.TO);
|
||||||
|
if (to != null && to.length > 0) {
|
||||||
|
addTableRow(html, context.getString(R.string.message_compose_quote_header_to),
|
||||||
|
Address.toString(to));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cc: <recipients>
|
||||||
|
Address[] cc = message.getRecipients(RecipientType.CC);
|
||||||
|
if (cc != null && cc.length > 0) {
|
||||||
|
addTableRow(html, context.getString(R.string.message_compose_quote_header_cc),
|
||||||
|
Address.toString(cc));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date: <date>
|
||||||
|
Date date = message.getSentDate();
|
||||||
|
if (date != null) {
|
||||||
|
addTableRow(html, context.getString(R.string.message_compose_quote_header_send_date),
|
||||||
|
date.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject: <subject>
|
||||||
|
String subject = message.getSubject();
|
||||||
|
addTableRow(html, context.getString(R.string.message_compose_quote_header_subject),
|
||||||
|
(subject == null) ? context.getString(R.string.general_no_subject) : subject);
|
||||||
|
|
||||||
|
html.append("</table>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output an HTML table two column row with some hardcoded style.
|
||||||
|
*
|
||||||
|
* @param html
|
||||||
|
* The {@link StringBuilder} that will receive the output.
|
||||||
|
* @param header
|
||||||
|
* The string to be put in the {@code TH} element.
|
||||||
|
* @param value
|
||||||
|
* The string to be put in the {@code TD} element.
|
||||||
|
*/
|
||||||
|
private static void addTableRow(StringBuilder html, String header, String value) {
|
||||||
|
html.append("<tr><th style=\"text-align: left; vertical-align: top;\">");
|
||||||
|
html.append(header);
|
||||||
|
html.append("</th>");
|
||||||
|
html.append("<td>");
|
||||||
|
html.append(value);
|
||||||
|
html.append("</td></tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the contents of a {@link Viewable} to create the plain text to be displayed.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This will use {@link HtmlConverter#htmlToText(String)} to convert HTML parts to plain text
|
||||||
|
* if necessary.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param viewable
|
||||||
|
* The viewable part to build the text from.
|
||||||
|
* @param prependDivider
|
||||||
|
* {@code true}, if the text divider should be inserted as first element.
|
||||||
|
* {@code false}, otherwise.
|
||||||
|
*
|
||||||
|
* @return The contents of the supplied viewable instance as plain text.
|
||||||
|
*/
|
||||||
|
private static StringBuilder buildText(Viewable viewable, boolean prependDivider)
|
||||||
|
{
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
if (viewable instanceof Textual) {
|
||||||
|
Part part = ((Textual)viewable).getPart();
|
||||||
|
addTextDivider(text, part, prependDivider);
|
||||||
|
|
||||||
|
String t = getTextFromPart(part);
|
||||||
|
if (t == null) {
|
||||||
|
t = "";
|
||||||
|
} else if (viewable instanceof Html) {
|
||||||
|
t = HtmlConverter.htmlToText(t);
|
||||||
|
}
|
||||||
|
text.append(t);
|
||||||
|
} else if (viewable instanceof Alternative) {
|
||||||
|
// That's odd - an Alternative as child of an Alternative; go ahead and try to use the
|
||||||
|
// text/plain child; fall-back to the text/html part.
|
||||||
|
Alternative alternative = (Alternative) viewable;
|
||||||
|
|
||||||
|
List<Viewable> textAlternative = alternative.getText().isEmpty() ?
|
||||||
|
alternative.getHtml() : alternative.getText();
|
||||||
|
|
||||||
|
boolean divider = prependDivider;
|
||||||
|
for (Viewable textViewable : textAlternative) {
|
||||||
|
text.append(buildText(textViewable, divider));
|
||||||
|
divider = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Some constants that are used by addTextDivider() below.
|
||||||
|
*/
|
||||||
|
private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length();
|
||||||
|
private static final String FILENAME_PREFIX = "----- ";
|
||||||
|
private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length();
|
||||||
|
private static final String FILENAME_SUFFIX = " ";
|
||||||
|
private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a plain text divider between two plain text message parts.
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
* The {@link StringBuilder} to append the divider to.
|
||||||
|
* @param part
|
||||||
|
* The message part that will follow after the divider. This is used to extract the
|
||||||
|
* part's name.
|
||||||
|
* @param prependDivider
|
||||||
|
* {@code true}, if the divider should be appended. {@code false}, otherwise.
|
||||||
|
*/
|
||||||
|
private static void addTextDivider(StringBuilder text, Part part, boolean prependDivider) {
|
||||||
|
if (prependDivider) {
|
||||||
|
String filename = getPartName(part);
|
||||||
|
|
||||||
|
text.append("\n\n");
|
||||||
|
int len = filename.length();
|
||||||
|
if (len > 0) {
|
||||||
|
if (len > TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - FILENAME_SUFFIX_LENGTH) {
|
||||||
|
filename = filename.substring(0, TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH -
|
||||||
|
FILENAME_SUFFIX_LENGTH - 3) + "...";
|
||||||
|
}
|
||||||
|
text.append(FILENAME_PREFIX);
|
||||||
|
text.append(filename);
|
||||||
|
text.append(FILENAME_SUFFIX);
|
||||||
|
text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH -
|
||||||
|
FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH));
|
||||||
|
} else {
|
||||||
|
text.append(TEXT_DIVIDER);
|
||||||
|
}
|
||||||
|
text.append("\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the contents of a {@link Viewable} to create the HTML to be displayed.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This will use {@link HtmlConverter#textToHtml(String, boolean)} to convert plain text parts
|
||||||
|
* to HTML if necessary.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param viewable
|
||||||
|
* The viewable part to build the HTML from.
|
||||||
|
* @param prependDivider
|
||||||
|
* {@code true}, if the HTML divider should be inserted as first element.
|
||||||
|
* {@code false}, otherwise.
|
||||||
|
*
|
||||||
|
* @return The contents of the supplied viewable instance as HTML.
|
||||||
|
*/
|
||||||
|
private static StringBuilder buildHtml(Viewable viewable, boolean prependDivider)
|
||||||
|
{
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
if (viewable instanceof Textual) {
|
||||||
|
Part part = ((Textual)viewable).getPart();
|
||||||
|
addHtmlDivider(html, part, prependDivider);
|
||||||
|
|
||||||
|
String t = getTextFromPart(part);
|
||||||
|
if (t == null) {
|
||||||
|
t = "";
|
||||||
|
} else if (viewable instanceof Text) {
|
||||||
|
t = HtmlConverter.textToHtml(t, false);
|
||||||
|
}
|
||||||
|
html.append(t);
|
||||||
|
} else if (viewable instanceof Alternative) {
|
||||||
|
// That's odd - an Alternative as child of an Alternative; go ahead and try to use the
|
||||||
|
// text/html child; fall-back to the text/plain part.
|
||||||
|
Alternative alternative = (Alternative) viewable;
|
||||||
|
|
||||||
|
List<Viewable> htmlAlternative = alternative.getHtml().isEmpty() ?
|
||||||
|
alternative.getText() : alternative.getHtml();
|
||||||
|
|
||||||
|
boolean divider = prependDivider;
|
||||||
|
for (Viewable htmlViewable : htmlAlternative) {
|
||||||
|
html.append(buildHtml(htmlViewable, divider));
|
||||||
|
divider = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an HTML divider between two HTML message parts.
|
||||||
|
*
|
||||||
|
* @param html
|
||||||
|
* The {@link StringBuilder} to append the divider to.
|
||||||
|
* @param part
|
||||||
|
* The message part that will follow after the divider. This is used to extract the
|
||||||
|
* part's name.
|
||||||
|
* @param prependDivider
|
||||||
|
* {@code true}, if the divider should be appended. {@code false}, otherwise.
|
||||||
|
*/
|
||||||
|
private static void addHtmlDivider(StringBuilder html, Part part, boolean prependDivider) {
|
||||||
|
if (prependDivider) {
|
||||||
|
String filename = getPartName(part);
|
||||||
|
|
||||||
|
html.append("<p style=\"margin-top: 2.5em; margin-bottom: 1em; border-bottom: 1px solid #000\">");
|
||||||
|
html.append(filename);
|
||||||
|
html.append("</p>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the message part.
|
||||||
|
*
|
||||||
|
* @param part
|
||||||
|
* The part to get the name for.
|
||||||
|
*
|
||||||
|
* @return The (file)name of the part if available. An empty string, otherwise.
|
||||||
|
*/
|
||||||
|
private static String getPartName(Part part) {
|
||||||
|
try {
|
||||||
|
String disposition = part.getDisposition();
|
||||||
|
if (disposition != null) {
|
||||||
|
String name = MimeUtility.getHeaderParameter(disposition, "filename");
|
||||||
|
return (name == null) ? "" : name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (MessagingException e) { /* ignore */ }
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of the {@code Content-Disposition} header.
|
||||||
|
*
|
||||||
|
* @param part
|
||||||
|
* The message part to read the header from.
|
||||||
|
*
|
||||||
|
* @return The value of the {@code Content-Disposition} header if available. {@code null},
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
private static String getContentDisposition(Part part) {
|
||||||
|
try {
|
||||||
|
String disposition = part.getDisposition();
|
||||||
|
if (disposition != null) {
|
||||||
|
return MimeUtility.getHeaderParameter(disposition, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (MessagingException e) { /* ignore */ }
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static Boolean isPartTextualBody(Part part) throws MessagingException {
|
public static Boolean isPartTextualBody(Part part) throws MessagingException {
|
||||||
String disposition = part.getDisposition();
|
String disposition = part.getDisposition();
|
||||||
|
@ -55,6 +55,7 @@ import com.fsck.k9.mail.internet.MimeHeader;
|
|||||||
import com.fsck.k9.mail.internet.MimeMessage;
|
import com.fsck.k9.mail.internet.MimeMessage;
|
||||||
import com.fsck.k9.mail.internet.MimeMultipart;
|
import com.fsck.k9.mail.internet.MimeMultipart;
|
||||||
import com.fsck.k9.mail.internet.MimeUtility;
|
import com.fsck.k9.mail.internet.MimeUtility;
|
||||||
|
import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer;
|
||||||
import com.fsck.k9.mail.internet.TextBody;
|
import com.fsck.k9.mail.internet.TextBody;
|
||||||
import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
|
import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
|
||||||
import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
|
import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
|
||||||
@ -2099,45 +2100,14 @@ public class LocalStore extends Store implements Serializable {
|
|||||||
deleteAttachments(message.getUid());
|
deleteAttachments(message.getUid());
|
||||||
}
|
}
|
||||||
|
|
||||||
ArrayList<Part> viewables = new ArrayList<Part>();
|
ViewableContainer container =
|
||||||
ArrayList<Part> attachments = new ArrayList<Part>();
|
MimeUtility.extractTextAndAttachments(mApplication, message);
|
||||||
MimeUtility.collectParts(message, viewables, attachments);
|
|
||||||
|
|
||||||
StringBuilder sbHtml = new StringBuilder();
|
List<Part> attachments = container.attachments;
|
||||||
StringBuilder sbText = new StringBuilder();
|
String text = container.text;
|
||||||
for (Part viewable : viewables) {
|
String html = HtmlConverter.convertEmoji2Img(container.html);
|
||||||
try {
|
|
||||||
String text = MimeUtility.getTextFromPart(viewable);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Small hack to make sure the string "null" doesn't end up
|
|
||||||
* in one of the StringBuilders.
|
|
||||||
*/
|
|
||||||
if (text == null) {
|
|
||||||
text = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Anything with MIME type text/html will be stored as such. Anything
|
|
||||||
* else will be stored as text/plain.
|
|
||||||
*/
|
|
||||||
if (viewable.getMimeType().equalsIgnoreCase("text/html")) {
|
|
||||||
sbHtml.append(text);
|
|
||||||
} else {
|
|
||||||
sbText.append(text);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new MessagingException("Unable to get text for message part", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String text = sbText.toString();
|
|
||||||
String html = markupContent(text, sbHtml.toString());
|
|
||||||
String preview = calculateContentPreview(text);
|
String preview = calculateContentPreview(text);
|
||||||
// If we couldn't generate a reasonable preview from the text part, try doing it with the HTML part.
|
|
||||||
if (preview == null || preview.length() == 0) {
|
|
||||||
preview = calculateContentPreview(HtmlConverter.htmlToText(html));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ContentValues cv = new ContentValues();
|
ContentValues cv = new ContentValues();
|
||||||
@ -2215,49 +2185,17 @@ public class LocalStore extends Store implements Serializable {
|
|||||||
@Override
|
@Override
|
||||||
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
|
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
|
||||||
try {
|
try {
|
||||||
ArrayList<Part> viewables = new ArrayList<Part>();
|
|
||||||
ArrayList<Part> attachments = new ArrayList<Part>();
|
|
||||||
|
|
||||||
message.buildMimeRepresentation();
|
message.buildMimeRepresentation();
|
||||||
|
|
||||||
MimeUtility.collectParts(message, viewables, attachments);
|
ViewableContainer container =
|
||||||
|
MimeUtility.extractTextAndAttachments(mApplication, message);
|
||||||
|
|
||||||
StringBuilder sbHtml = new StringBuilder();
|
List<Part> attachments = container.attachments;
|
||||||
StringBuilder sbText = new StringBuilder();
|
String text = container.text;
|
||||||
for (int i = 0, count = viewables.size(); i < count; i++) {
|
String html = HtmlConverter.convertEmoji2Img(container.html);
|
||||||
Part viewable = viewables.get(i);
|
|
||||||
try {
|
|
||||||
String text = MimeUtility.getTextFromPart(viewable);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Small hack to make sure the string "null" doesn't end up
|
|
||||||
* in one of the StringBuilders.
|
|
||||||
*/
|
|
||||||
if (text == null) {
|
|
||||||
text = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Anything with MIME type text/html will be stored as such. Anything
|
|
||||||
* else will be stored as text/plain.
|
|
||||||
*/
|
|
||||||
if (viewable.getMimeType().equalsIgnoreCase("text/html")) {
|
|
||||||
sbHtml.append(text);
|
|
||||||
} else {
|
|
||||||
sbText.append(text);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new MessagingException("Unable to get text for message part", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String text = sbText.toString();
|
|
||||||
String html = markupContent(text, sbHtml.toString());
|
|
||||||
String preview = calculateContentPreview(text);
|
String preview = calculateContentPreview(text);
|
||||||
// If we couldn't generate a reasonable preview from the text part, try doing it with the HTML part.
|
|
||||||
if (preview == null || preview.length() == 0) {
|
|
||||||
preview = calculateContentPreview(HtmlConverter.htmlToText(html));
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
db.execSQL("UPDATE messages SET "
|
db.execSQL("UPDATE messages SET "
|
||||||
+ "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, "
|
+ "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, "
|
||||||
@ -2391,6 +2329,18 @@ public class LocalStore extends Store implements Serializable {
|
|||||||
Body body = attachment.getBody();
|
Body body = attachment.getBody();
|
||||||
if (body instanceof LocalAttachmentBody) {
|
if (body instanceof LocalAttachmentBody) {
|
||||||
contentUri = ((LocalAttachmentBody) body).getContentUri();
|
contentUri = ((LocalAttachmentBody) body).getContentUri();
|
||||||
|
} else if (body instanceof Message) {
|
||||||
|
// It's a message, so use Message.writeTo() to output the
|
||||||
|
// message including all children.
|
||||||
|
Message message = (Message) body;
|
||||||
|
tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory);
|
||||||
|
FileOutputStream out = new FileOutputStream(tempAttachmentFile);
|
||||||
|
try {
|
||||||
|
message.writeTo(out);
|
||||||
|
} finally {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
size = (int) (tempAttachmentFile.length() & 0x7FFFFFFFL);
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* If the attachment has a body we're expected to save it into the local store
|
* If the attachment has a body we're expected to save it into the local store
|
||||||
@ -2804,17 +2754,6 @@ public class LocalStore extends Store implements Serializable {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String markupContent(String text, String html) {
|
|
||||||
if (text.length() > 0 && html.length() == 0) {
|
|
||||||
html = HtmlConverter.textToHtml(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
html = HtmlConverter.convertEmoji2Img(html);
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isInTopGroup() {
|
public boolean isInTopGroup() {
|
||||||
return mInTopGroup;
|
return mInTopGroup;
|
||||||
|
188
tests/src/com/fsck/k9/mail/internet/ViewablesTest.java
Normal file
188
tests/src/com/fsck/k9/mail/internet/ViewablesTest.java
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package com.fsck.k9.mail.internet;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import android.test.AndroidTestCase;
|
||||||
|
import com.fsck.k9.mail.Address;
|
||||||
|
import com.fsck.k9.mail.MessagingException;
|
||||||
|
import com.fsck.k9.mail.Message.RecipientType;
|
||||||
|
import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer;
|
||||||
|
|
||||||
|
public class ViewablesTest extends AndroidTestCase {
|
||||||
|
|
||||||
|
public void testSimplePlainTextMessage() throws MessagingException {
|
||||||
|
String bodyText = "K-9 Mail rocks :>";
|
||||||
|
|
||||||
|
// Create text/plain body
|
||||||
|
TextBody body = new TextBody(bodyText);
|
||||||
|
|
||||||
|
// Create message
|
||||||
|
MimeMessage message = new MimeMessage();
|
||||||
|
message.setBody(body);
|
||||||
|
|
||||||
|
// Extract text
|
||||||
|
ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
|
||||||
|
|
||||||
|
String expectedText = bodyText;
|
||||||
|
String expectedHtml =
|
||||||
|
"<html><head/><body>" +
|
||||||
|
"<pre style=\"white-space: pre-wrap; word-wrap:break-word; " +
|
||||||
|
"font-family: sans-serif\">" +
|
||||||
|
"K-9 Mail rocks :>" +
|
||||||
|
"</pre>" +
|
||||||
|
"</body></html>";
|
||||||
|
|
||||||
|
assertEquals(expectedText, container.text);
|
||||||
|
assertEquals(expectedHtml, container.html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testSimpleHtmlMessage() throws MessagingException {
|
||||||
|
String bodyText = "<strong>K-9 Mail</strong> rocks :>";
|
||||||
|
|
||||||
|
// Create text/plain body
|
||||||
|
TextBody body = new TextBody(bodyText);
|
||||||
|
|
||||||
|
// Create message
|
||||||
|
MimeMessage message = new MimeMessage();
|
||||||
|
message.setHeader("Content-Type", "text/html");
|
||||||
|
message.setBody(body);
|
||||||
|
|
||||||
|
// Extract text
|
||||||
|
ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
|
||||||
|
|
||||||
|
String expectedText = "K-9 Mail rocks :>";
|
||||||
|
String expectedHtml =
|
||||||
|
"<html><head/><body>" +
|
||||||
|
bodyText +
|
||||||
|
"</body></html>";
|
||||||
|
|
||||||
|
assertEquals(expectedText, container.text);
|
||||||
|
assertEquals(expectedHtml, container.html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMultipartPlainTextMessage() throws MessagingException {
|
||||||
|
String bodyText1 = "text body 1";
|
||||||
|
String bodyText2 = "text body 2";
|
||||||
|
|
||||||
|
// Create text/plain bodies
|
||||||
|
TextBody body1 = new TextBody(bodyText1);
|
||||||
|
TextBody body2 = new TextBody(bodyText2);
|
||||||
|
|
||||||
|
// Create multipart/mixed part
|
||||||
|
MimeMultipart multipart = new MimeMultipart();
|
||||||
|
MimeBodyPart bodyPart1 = new MimeBodyPart(body1, "text/plain");
|
||||||
|
MimeBodyPart bodyPart2 = new MimeBodyPart(body2, "text/plain");
|
||||||
|
multipart.addBodyPart(bodyPart1);
|
||||||
|
multipart.addBodyPart(bodyPart2);
|
||||||
|
|
||||||
|
// Create message
|
||||||
|
MimeMessage message = new MimeMessage();
|
||||||
|
message.setBody(multipart);
|
||||||
|
|
||||||
|
// Extract text
|
||||||
|
ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
|
||||||
|
|
||||||
|
String expectedText =
|
||||||
|
bodyText1 + "\n\n" +
|
||||||
|
"------------------------------------------------------------------------\n\n" +
|
||||||
|
bodyText2;
|
||||||
|
String expectedHtml =
|
||||||
|
"<html><head/><body>" +
|
||||||
|
"<pre style=\"white-space: pre-wrap; word-wrap:break-word; " +
|
||||||
|
"font-family: sans-serif\">" +
|
||||||
|
bodyText1 +
|
||||||
|
"</pre>" +
|
||||||
|
"<p style=\"margin-top: 2.5em; margin-bottom: 1em; " +
|
||||||
|
"border-bottom: 1px solid #000\"></p>" +
|
||||||
|
"<pre style=\"white-space: pre-wrap; word-wrap:break-word; " +
|
||||||
|
"font-family: sans-serif\">" +
|
||||||
|
bodyText2 +
|
||||||
|
"</pre>" +
|
||||||
|
"</body></html>";
|
||||||
|
|
||||||
|
|
||||||
|
assertEquals(expectedText, container.text);
|
||||||
|
assertEquals(expectedHtml, container.html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTextPlusRfc822Message() throws MessagingException {
|
||||||
|
Locale.setDefault(Locale.US);
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
|
||||||
|
|
||||||
|
String bodyText = "Some text here";
|
||||||
|
String innerBodyText = "Hey there. I'm inside a message/rfc822 (inline) attachment.";
|
||||||
|
|
||||||
|
// Create text/plain body
|
||||||
|
TextBody textBody = new TextBody(bodyText);
|
||||||
|
|
||||||
|
// Create inner text/plain body
|
||||||
|
TextBody innerBody = new TextBody(innerBodyText);
|
||||||
|
|
||||||
|
// Create message/rfc822 body
|
||||||
|
MimeMessage innerMessage = new MimeMessage();
|
||||||
|
innerMessage.addSentDate(new Date(112, 02, 17));
|
||||||
|
innerMessage.setRecipients(RecipientType.TO, new Address[] { new Address("to@example.com") });
|
||||||
|
innerMessage.setSubject("Subject");
|
||||||
|
innerMessage.setFrom(new Address("from@example.com"));
|
||||||
|
innerMessage.setBody(innerBody);
|
||||||
|
|
||||||
|
// Create multipart/mixed part
|
||||||
|
MimeMultipart multipart = new MimeMultipart();
|
||||||
|
MimeBodyPart bodyPart1 = new MimeBodyPart(textBody, "text/plain");
|
||||||
|
MimeBodyPart bodyPart2 = new MimeBodyPart(innerMessage, "message/rfc822");
|
||||||
|
bodyPart2.setHeader("Content-Disposition", "inline; filename=\"message.eml\"");
|
||||||
|
multipart.addBodyPart(bodyPart1);
|
||||||
|
multipart.addBodyPart(bodyPart2);
|
||||||
|
|
||||||
|
// Create message
|
||||||
|
MimeMessage message = new MimeMessage();
|
||||||
|
message.setBody(multipart);
|
||||||
|
|
||||||
|
// Extract text
|
||||||
|
ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message);
|
||||||
|
|
||||||
|
String expectedText =
|
||||||
|
bodyText +
|
||||||
|
"\n\n" +
|
||||||
|
"----- message.eml ------------------------------------------------------" +
|
||||||
|
"\n\n" +
|
||||||
|
"From: from@example.com" + "\n" +
|
||||||
|
"To: to@example.com" + "\n" +
|
||||||
|
"Sent: Sat Mar 17 00:00:00 GMT+00:00 2012" + "\n" +
|
||||||
|
"Subject: Subject" + "\n" +
|
||||||
|
"\n" +
|
||||||
|
innerBodyText;
|
||||||
|
String expectedHtml =
|
||||||
|
"<html><head/><body>" +
|
||||||
|
"<pre style=\"white-space: pre-wrap; word-wrap:break-word; " +
|
||||||
|
"font-family: sans-serif\">" +
|
||||||
|
bodyText +
|
||||||
|
"</pre>" +
|
||||||
|
"<p style=\"margin-top: 2.5em; margin-bottom: 1em; border-bottom: " +
|
||||||
|
"1px solid #000\">message.eml</p>" +
|
||||||
|
"<table style=\"border: 0\">" +
|
||||||
|
"<tr>" +
|
||||||
|
"<th style=\"text-align: left; vertical-align: top;\">From:</th>" +
|
||||||
|
"<td>from@example.com</td>" +
|
||||||
|
"</tr><tr>" +
|
||||||
|
"<th style=\"text-align: left; vertical-align: top;\">To:</th>" +
|
||||||
|
"<td>to@example.com</td>" +
|
||||||
|
"</tr><tr>" +
|
||||||
|
"<th style=\"text-align: left; vertical-align: top;\">Sent:</th>" +
|
||||||
|
"<td>Sat Mar 17 00:00:00 GMT+00:00 2012</td>" +
|
||||||
|
"</tr><tr>" +
|
||||||
|
"<th style=\"text-align: left; vertical-align: top;\">Subject:</th>" +
|
||||||
|
"<td>Subject</td>" +
|
||||||
|
"</tr>" +
|
||||||
|
"</table>" +
|
||||||
|
"<pre style=\"white-space: pre-wrap; word-wrap:break-word; " +
|
||||||
|
"font-family: sans-serif\">" +
|
||||||
|
innerBodyText +
|
||||||
|
"</pre>" +
|
||||||
|
"</body></html>";
|
||||||
|
|
||||||
|
assertEquals(expectedText, container.text);
|
||||||
|
assertEquals(expectedHtml, container.html);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user