2014-12-15 06:05:21 -05:00
|
|
|
package com.fsck.k9.mailstore;
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
|
|
|
|
import com.fsck.k9.R;
|
|
|
|
import com.fsck.k9.mail.Address;
|
|
|
|
import com.fsck.k9.mail.Body;
|
|
|
|
import com.fsck.k9.mail.BodyPart;
|
|
|
|
import com.fsck.k9.mail.Message;
|
|
|
|
import com.fsck.k9.mail.MessagingException;
|
|
|
|
import com.fsck.k9.mail.Part;
|
|
|
|
import com.fsck.k9.helper.HtmlConverter;
|
|
|
|
import com.fsck.k9.mail.internet.MessageExtractor;
|
|
|
|
import com.fsck.k9.mail.internet.MimeMultipart;
|
|
|
|
import com.fsck.k9.mail.internet.Viewable;
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Date;
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter;
|
|
|
|
|
|
|
|
class LocalMessageExtractor {
|
|
|
|
private static final String TEXT_DIVIDER =
|
|
|
|
"------------------------------------------------------------------------";
|
|
|
|
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();
|
|
|
|
|
2014-12-15 06:42:05 -05:00
|
|
|
private LocalMessageExtractor() {}
|
2014-12-15 06:05:21 -05:00
|
|
|
/**
|
|
|
|
* Extract the viewable textual parts of a message and return the rest as attachments.
|
|
|
|
*
|
|
|
|
* @param context A {@link android.content.Context} instance that will be used to get localized strings.
|
|
|
|
* @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 com.fsck.k9.mail.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 = MessageExtractor.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();
|
|
|
|
|
|
|
|
for (Viewable viewable : viewables) {
|
|
|
|
if (viewable instanceof Viewable.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 Viewable.MessageHeader) {
|
|
|
|
Viewable.MessageHeader header = (Viewable.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 Viewable.Alternative) {
|
|
|
|
// Handle multipart/alternative contents
|
|
|
|
Viewable.Alternative alternative = (Viewable.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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new ViewableContainer(text.toString(), html.toString(), attachments);
|
|
|
|
} catch (Exception e) {
|
|
|
|
throw new MessagingException("Couldn't extract viewable parts", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static ViewableContainer extractPartsFromDraft(Message message)
|
|
|
|
throws MessagingException {
|
|
|
|
|
|
|
|
Body body = message.getBody();
|
|
|
|
if (message.isMimeType("multipart/mixed") && body instanceof MimeMultipart) {
|
|
|
|
MimeMultipart multipart = (MimeMultipart) body;
|
|
|
|
|
|
|
|
ViewableContainer container;
|
|
|
|
int count = multipart.getCount();
|
|
|
|
if (count >= 1) {
|
|
|
|
// The first part is either a text/plain or a multipart/alternative
|
|
|
|
BodyPart firstPart = multipart.getBodyPart(0);
|
|
|
|
container = extractTextual(firstPart);
|
|
|
|
|
|
|
|
// The rest should be attachments
|
|
|
|
for (int i = 1; i < count; i++) {
|
|
|
|
BodyPart bodyPart = multipart.getBodyPart(i);
|
|
|
|
container.attachments.add(bodyPart);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
container = new ViewableContainer("", "", new ArrayList<Part>());
|
|
|
|
}
|
|
|
|
|
|
|
|
return container;
|
|
|
|
}
|
|
|
|
|
|
|
|
return extractTextual(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use the contents of a {@link com.fsck.k9.mail.internet.Viewable} to create the HTML to be displayed.
|
|
|
|
*
|
|
|
|
* <p>
|
|
|
|
* This will use {@link com.fsck.k9.helper.HtmlConverter#textToHtml(String)} 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 Viewable.Textual) {
|
|
|
|
Part part = ((Viewable.Textual)viewable).getPart();
|
|
|
|
addHtmlDivider(html, part, prependDivider);
|
|
|
|
|
|
|
|
String t = part.getText();
|
|
|
|
if (t == null) {
|
|
|
|
t = "";
|
|
|
|
} else if (viewable instanceof Viewable.Text) {
|
|
|
|
t = HtmlConverter.textToHtml(t);
|
|
|
|
}
|
|
|
|
html.append(t);
|
|
|
|
} else if (viewable instanceof Viewable.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.
|
|
|
|
Viewable.Alternative alternative = (Viewable.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static StringBuilder buildText(Viewable viewable, boolean prependDivider)
|
|
|
|
{
|
|
|
|
StringBuilder text = new StringBuilder();
|
|
|
|
if (viewable instanceof Viewable.Textual) {
|
|
|
|
Part part = ((Viewable.Textual)viewable).getPart();
|
|
|
|
addTextDivider(text, part, prependDivider);
|
|
|
|
|
|
|
|
String t = part.getText();
|
|
|
|
if (t == null) {
|
|
|
|
t = "";
|
|
|
|
} else if (viewable instanceof Viewable.Html) {
|
|
|
|
t = HtmlConverter.htmlToText(t);
|
|
|
|
}
|
|
|
|
text.append(t);
|
|
|
|
} else if (viewable instanceof Viewable.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.
|
|
|
|
Viewable.Alternative alternative = (Viewable.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 = getHeaderParameter(disposition, "filename");
|
|
|
|
return (name == null) ? "" : name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (MessagingException e) { /* ignore */ }
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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("\r\n\r\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("\r\n\r\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract important header values from a message to display inline (plain text version).
|
|
|
|
*
|
|
|
|
* @param context
|
|
|
|
* A {@link android.content.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 com.fsck.k9.mail.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("\r\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
// To: <recipients>
|
|
|
|
Address[] to = message.getRecipients(Message.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("\r\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cc: <recipients>
|
|
|
|
Address[] cc = message.getRecipients(Message.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("\r\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("\r\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("\r\n\r\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract important header values from a message to display inline (HTML version).
|
|
|
|
*
|
|
|
|
* @param context
|
|
|
|
* A {@link android.content.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 com.fsck.k9.mail.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(Message.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(Message.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>");
|
|
|
|
}
|
|
|
|
|
|
|
|
private static ViewableContainer extractTextual(Part part) throws MessagingException {
|
|
|
|
String text = "";
|
|
|
|
String html = "";
|
|
|
|
List<Part> attachments = new ArrayList<Part>();
|
|
|
|
|
|
|
|
Body firstBody = part.getBody();
|
|
|
|
if (part.isMimeType("text/plain")) {
|
|
|
|
String bodyText = part.getText();
|
|
|
|
if (bodyText != null) {
|
|
|
|
text = bodyText;
|
|
|
|
html = HtmlConverter.textToHtml(text);
|
|
|
|
}
|
|
|
|
} else if (part.isMimeType("multipart/alternative") &&
|
|
|
|
firstBody instanceof MimeMultipart) {
|
|
|
|
MimeMultipart multipart = (MimeMultipart) firstBody;
|
|
|
|
for (BodyPart bodyPart : multipart.getBodyParts()) {
|
|
|
|
String bodyText = bodyPart.getText();
|
|
|
|
if (bodyText != null) {
|
|
|
|
if (text.isEmpty() && bodyPart.isMimeType("text/plain")) {
|
|
|
|
text = bodyText;
|
|
|
|
} else if (html.isEmpty() && bodyPart.isMimeType("text/html")) {
|
|
|
|
html = bodyText;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new ViewableContainer(text, html, attachments);
|
|
|
|
}
|
|
|
|
}
|