Merge branch 'viewables'

This commit is contained in:
cketti 2012-02-18 19:29:34 +01:00
commit 8e32320f5d
6 changed files with 1135 additions and 138 deletions

View file

@ -2709,7 +2709,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, OnFoc
if (part != null) {
if (K9.DEBUG)
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) {
// Text takes precedence, then html.

View file

@ -1691,9 +1691,7 @@ public class MessagingController implements Runnable {
* right now, attachments will be left for later.
*/
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
Set<Part> viewables = MimeUtility.collectTextParts(message);
/*
* Now download the parts we're interested in storing.
@ -2816,9 +2814,7 @@ public class MessagingController implements Runnable {
try {
LocalStore localStore = account.getLocalStore();
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
List<Part> attachments = MimeUtility.collectAttachments(message);
for (Part attachment : attachments) {
attachment.setBody(null);
}

View file

@ -125,19 +125,41 @@ public class HtmlConverter {
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
* message body to save memory.
* @param text Plain text string.
* Naively convert a text string into an HTML document.
*
* <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.
*/
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.
text = TextUtils.htmlEncode(text);
StringReader reader = new StringReader(text);
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;
try {
@ -159,25 +181,39 @@ public class HtmlConverter {
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();
}
/**
* Convert a text string into an HTML document. Attempts to do smart replacement for large
* 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)}.
* @param text Plain text string.
* Convert a text string into an HTML document.
*
* <p>
* 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.
*/
public static String textToHtml(String text) {
public static String textToHtml(String text, boolean useHtmlTag) {
// Our HTMLification code is somewhat memory intensive
// and was causing lots of OOM errors on the market
// if the message is big and plain text, just do
// a trivial htmlification
if (text.length() > MAX_SMART_HTMLIFY_MESSAGE_LENGTH) {
return simpleTextToHtml(text);
return simpleTextToHtml(text, useHtmlTag);
}
StringReader reader = new StringReader(text);
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");
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());
linkifyText(text, sb);
sb.append(htmlifyMessageFooter());
sb.append("</body></html>");
if (useHtmlTag) {
sb.append(getHtmlFooter());
}
text = sb.toString();
return text;

View file

@ -1,9 +1,14 @@
package com.fsck.k9.mail.internet;
import android.content.Context;
import android.util.Log;
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.Message.RecipientType;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.codec.Base64InputStream;
import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
@ -12,7 +17,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
@ -23,6 +32,9 @@ public class MimeUtility {
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
* +
@ -1100,49 +1112,867 @@ public class MimeUtility {
return tempBody;
}
/**
* An unfortunately named method that makes decisions about a Part (usually a Message)
* as to which of it's children will be "viewable" and which will be attachments.
* The method recursively sorts the viewables and attachments into seperate
* lists for further processing.
* @param part
* @param viewables
* @param attachments
* @throws MessagingException
* Empty base class for the class hierarchy used by
* {@link MimeUtility#extractTextAndAttachments(Context, Message)}.
*
* @see Text
* @see Html
* @see MessageHeader
* @see Alternative
*/
public static void collectParts(Part part, ArrayList<Part> viewables,
ArrayList<Part> attachments) throws MessagingException {
/*
* If the part is Multipart but not alternative it's either mixed or
* something we don't know about, which means we treat it as mixed
* per the spec. We just process it's pieces recursively.
static abstract class Viewable { /* empty */ }
/**
* Class representing textual parts of a message that aren't marked as attachments.
*
* @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) {
Multipart mp = (Multipart)part.getBody();
for (int i = 0; i < mp.getCount(); i++) {
collectParts(mp.getBodyPart(i), viewables, attachments);
public final String text;
/**
* 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.
*/
else if (part.getBody() instanceof Message) {
Message message = (Message)part.getBody();
collectParts(message, viewables, attachments);
}
/*
* If the part is HTML and it got this far it's part of a mixed (et
* al) and should be rendered inline.
*/
else if (isPartTextualBody(part)) {
viewables.add(part);
}
/**
* Traverse the MIME tree of a message an extract viewable parts.
*
* @param part
* The message part to start from.
* @param attachments
* A list that will receive the parts that are considered attachments.
*
* @return A list of {@link Viewable}s.
*
* @throws MessagingException
* 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 {
// Everything else is treated as attachment.
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 {
String disposition = part.getDisposition();

View file

@ -55,6 +55,7 @@ import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
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.store.LockableDatabase.DbCallback;
import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
@ -2099,45 +2100,14 @@ public class LocalStore extends Store implements Serializable {
deleteAttachments(message.getUid());
}
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
ViewableContainer container =
MimeUtility.extractTextAndAttachments(mApplication, message);
StringBuilder sbHtml = new StringBuilder();
StringBuilder sbText = new StringBuilder();
for (Part viewable : viewables) {
try {
String text = MimeUtility.getTextFromPart(viewable);
List<Part> attachments = container.attachments;
String text = container.text;
String html = HtmlConverter.convertEmoji2Img(container.html);
/*
* 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);
// 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 {
ContentValues cv = new ContentValues();
@ -2215,49 +2185,17 @@ public class LocalStore extends Store implements Serializable {
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
try {
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
message.buildMimeRepresentation();
MimeUtility.collectParts(message, viewables, attachments);
ViewableContainer container =
MimeUtility.extractTextAndAttachments(mApplication, message);
StringBuilder sbHtml = new StringBuilder();
StringBuilder sbText = new StringBuilder();
for (int i = 0, count = viewables.size(); i < count; i++) {
Part viewable = viewables.get(i);
try {
String text = MimeUtility.getTextFromPart(viewable);
List<Part> attachments = container.attachments;
String text = container.text;
String html = HtmlConverter.convertEmoji2Img(container.html);
/*
* 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);
// 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 {
db.execSQL("UPDATE messages SET "
+ "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, "
@ -2391,6 +2329,18 @@ public class LocalStore extends Store implements Serializable {
Body body = attachment.getBody();
if (body instanceof LocalAttachmentBody) {
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 {
/*
* 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
public boolean isInTopGroup() {
return mInTopGroup;

View 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 :&gt;" +
"</pre>" +
"</body></html>";
assertEquals(expectedText, container.text);
assertEquals(expectedHtml, container.html);
}
public void testSimpleHtmlMessage() throws MessagingException {
String bodyText = "<strong>K-9 Mail</strong> rocks :&gt;";
// 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);
}
}