Merge pull request #2159 from k9mail/jsoup_preparation

Code refactoring in preparation of HtmlCleaner replacement
This commit is contained in:
cketti 2017-02-04 23:36:26 +01:00 committed by GitHub
commit 2d84b41e5c
31 changed files with 519 additions and 265 deletions

View file

@ -39,6 +39,7 @@ dependencies {
testCompile "org.robolectric:robolectric:${robolectricVersion}"
testCompile "junit:junit:${junitVersion}"
testCompile "org.mockito:mockito-core:${mockitoVersion}"
testCompile 'org.jsoup:jsoup:1.10.2'
}
android {

View file

@ -6,13 +6,12 @@ import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import com.fsck.k9.R;
import com.fsck.k9.activity.Accounts;
import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.message.html.HtmlConverter;
/**
* Displays a welcome message when no accounts have been created yet.

View file

@ -15,8 +15,8 @@ import android.util.Log;
import com.fsck.k9.Globals;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.helper.HtmlSanitizer;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.message.html.HtmlSanitizer;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
@ -211,7 +211,7 @@ public class MessageViewInfoExtractor {
* 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
* This will use {@link HtmlConverter#textToHtml(String)} to convert plain text parts
* to HTML if necessary.
* </p>
*

View file

@ -10,6 +10,7 @@ import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.message.quote.InsertableHtmlContent;
public class IdentityHeaderBuilder {

View file

@ -33,6 +33,7 @@ import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.TempFileBody;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import org.apache.james.mime4j.codec.EncoderUtil;
import org.apache.james.mime4j.util.MimeUtil;

View file

@ -5,9 +5,10 @@ import android.text.TextUtils;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.message.quote.InsertableHtmlContent;
class TextBodyBuilder {

View file

@ -0,0 +1,62 @@
package com.fsck.k9.message.extractors;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.message.SimpleMessageFormat;
import com.fsck.k9.message.html.HtmlConverter;
//TODO: Get rid of this class and use MessageViewInfoExtractor instead
public class BodyTextExtractor {
/** Fetch the body text from a messagePart in the desired messagePart format. This method handles
* conversions between formats (html to text and vice versa) if necessary.
*/
public static String getBodyTextFromMessage(Part messagePart, SimpleMessageFormat format) {
Part part;
if (format == SimpleMessageFormat.HTML) {
// HTML takes precedence, then text.
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/html");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, HTML found.");
}
return MessageExtractor.getTextFromPart(part);
}
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/plain");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, text found.");
}
String text = MessageExtractor.getTextFromPart(part);
return HtmlConverter.textToHtml(text);
}
} else if (format == SimpleMessageFormat.TEXT) {
// Text takes precedence, then html.
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/plain");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, text found.");
}
return MessageExtractor.getTextFromPart(part);
}
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/html");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, HTML found.");
}
String text = MessageExtractor.getTextFromPart(part);
return HtmlConverter.htmlToText(text);
}
}
// If we had nothing interesting, return an empty string.
return "";
}
}

View file

@ -3,7 +3,7 @@ package com.fsck.k9.message.extractors;
import android.support.annotation.NonNull;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;

View file

@ -3,7 +3,7 @@ package com.fsck.k9.message.extractors;
import android.support.annotation.NonNull;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;

View file

@ -1,4 +1,4 @@
package com.fsck.k9.helper;
package com.fsck.k9.message.html;
import android.text.*;
import android.text.Html.TagHandler;

View file

@ -1,4 +1,4 @@
package com.fsck.k9.helper;
package com.fsck.k9.message.html;
import android.support.annotation.VisibleForTesting;

View file

@ -1,11 +1,6 @@
package com.fsck.k9.helper;
package com.fsck.k9.message.quote;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -19,21 +14,10 @@ import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.message.InsertableHtmlContent;
import com.fsck.k9.message.SimpleMessageFormat;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer;
import org.htmlcleaner.TagNode;
import com.fsck.k9.message.html.HtmlConverter;
public class QuotedMessageHelper {
// amount of extra buffer to allocate to accommodate quoting headers or prefixes
private static final int QUOTE_BUFFER_LENGTH = 512;
public class HtmlQuoteCreator {
// Regular expressions to look for various HTML tags. This is no HTML::Parser, but hopefully it's good enough for
// our purposes.
private static final Pattern FIND_INSERTION_POINT_HTML = Pattern.compile("(?si:.*?(<html(?:>|\\s+[^>]*>)).*)");
@ -41,13 +25,6 @@ public class QuotedMessageHelper {
private static final Pattern FIND_INSERTION_POINT_BODY = Pattern.compile("(?si:.*?(<body(?:>|\\s+[^>]*>)).*)");
private static final Pattern FIND_INSERTION_POINT_HTML_END = Pattern.compile("(?si:.*(</html>).*?)");
private static final Pattern FIND_INSERTION_POINT_BODY_END = Pattern.compile("(?si:.*(</body>).*?)");
// Regexes to check for signature.
private static final Pattern DASH_SIGNATURE_HTML = Pattern.compile("(<br( /)?>|\r?\n)-- <br( /)?>", Pattern.CASE_INSENSITIVE);
private static final Pattern BLOCKQUOTE_START = Pattern.compile("<blockquote", Pattern.CASE_INSENSITIVE);
private static final Pattern BLOCKQUOTE_END = Pattern.compile("</blockquote>", Pattern.CASE_INSENSITIVE);
private static final Pattern DASH_SIGNATURE_PLAIN = Pattern.compile("\r\n-- \r\n.*", Pattern.DOTALL);
// The first group in a Matcher contains the first capture group. We capture the tag found in the above REs so that
// we can locate the *end* of that tag.
private static final int FIND_INSERTION_POINT_FIRST_GROUP = 1;
@ -59,9 +36,7 @@ public class QuotedMessageHelper {
// Index of the start of the beginning of a String.
private static final int FIND_INSERTION_POINT_START_OF_STRING = 0;
private static final int REPLY_WRAP_LINE_WIDTH = 72;
/**
* Add quoting markup to a HTML message.
* @param originalMessage Metadata for message being quoted.
@ -74,10 +49,10 @@ public class QuotedMessageHelper {
String messageBody, QuoteStyle quoteStyle) throws MessagingException {
InsertableHtmlContent insertable = findInsertionPoints(messageBody);
String sentDate = getSentDateText(resources, originalMessage);
String sentDate = QuoteHelper.getSentDateText(resources, originalMessage);
String fromAddress = Address.toString(originalMessage.getFrom());
if (quoteStyle == QuoteStyle.PREFIX) {
StringBuilder header = new StringBuilder(QUOTE_BUFFER_LENGTH);
StringBuilder header = new StringBuilder(QuoteHelper.QUOTE_BUFFER_LENGTH);
header.append("<div class=\"gmail_quote\">");
if (sentDate.length() != 0) {
header.append(HtmlConverter.textToHtmlFragment(String.format(
@ -134,25 +109,6 @@ public class QuotedMessageHelper {
return insertable;
}
/**
* Extract the date from a message and convert it into a locale-specific
* date string suitable for use in a header for a quoted message.
*
* @return A string with the formatted date/time
*/
private static String getSentDateText(Resources resources, Message message) {
try {
final int dateStyle = DateFormat.LONG;
final int timeStyle = DateFormat.LONG;
Date date = message.getSentDate();
Locale locale = resources.getConfiguration().locale;
return DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale)
.format(date);
} catch (Exception e) {
return "";
}
}
/**
* <p>Find the start and end positions of the HTML in the string. This should be the very top
* and bottom of the displayable message. It returns a {@link InsertableHtmlContent}, which
@ -265,189 +221,4 @@ public class QuotedMessageHelper {
return insertable;
}
/**
* Add quoting markup to a text message.
* @param originalMessage Metadata for message being quoted.
* @param messageBody Text of the message to be quoted.
* @param quoteStyle Style of quoting.
* @return Quoted text.
* @throws MessagingException
*/
public static String quoteOriginalTextMessage(Resources resources, Message originalMessage, String messageBody, QuoteStyle quoteStyle, String prefix) throws MessagingException {
String body = messageBody == null ? "" : messageBody;
String sentDate = QuotedMessageHelper.getSentDateText(resources, originalMessage);
if (quoteStyle == QuoteStyle.PREFIX) {
StringBuilder quotedText = new StringBuilder(body.length() + QuotedMessageHelper.QUOTE_BUFFER_LENGTH);
if (sentDate.length() != 0) {
quotedText.append(String.format(
resources.getString(R.string.message_compose_reply_header_fmt_with_date) + "\r\n",
sentDate,
Address.toString(originalMessage.getFrom())));
} else {
quotedText.append(String.format(
resources.getString(R.string.message_compose_reply_header_fmt) + "\r\n",
Address.toString(originalMessage.getFrom()))
);
}
final String wrappedText = Utility.wrap(body, REPLY_WRAP_LINE_WIDTH - prefix.length());
// "$" and "\" in the quote prefix have to be escaped for
// the replaceAll() invocation.
final String escapedPrefix = prefix.replaceAll("(\\\\|\\$)", "\\\\$1");
quotedText.append(wrappedText.replaceAll("(?m)^", escapedPrefix));
// TODO is this correct?
return quotedText.toString().replaceAll("\\\r", "");
} else if (quoteStyle == QuoteStyle.HEADER) {
StringBuilder quotedText = new StringBuilder(body.length() + QuotedMessageHelper.QUOTE_BUFFER_LENGTH);
quotedText.append("\r\n");
quotedText.append(resources.getString(R.string.message_compose_quote_header_separator)).append("\r\n");
if (originalMessage.getFrom() != null && Address.toString(originalMessage.getFrom()).length() != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_from)).append(" ").append(Address.toString(originalMessage.getFrom())).append("\r\n");
}
if (sentDate.length() != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_send_date)).append(" ").append(sentDate).append("\r\n");
}
if (originalMessage.getRecipients(RecipientType.TO) != null && originalMessage.getRecipients(RecipientType.TO).length != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_to)).append(" ").append(Address.toString(originalMessage.getRecipients(RecipientType.TO))).append("\r\n");
}
if (originalMessage.getRecipients(RecipientType.CC) != null && originalMessage.getRecipients(RecipientType.CC).length != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_cc)).append(" ").append(Address.toString(originalMessage.getRecipients(RecipientType.CC))).append("\r\n");
}
if (originalMessage.getSubject() != null) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_subject)).append(" ").append(originalMessage.getSubject()).append("\r\n");
}
quotedText.append("\r\n");
quotedText.append(body);
return quotedText.toString();
} else {
// Shouldn't ever happen.
return body;
}
}
/** Fetch the body text from a messagePart in the desired messagePart format. This method handles
* conversions between formats (html to text and vice versa) if necessary.
*/
public static String getBodyTextFromMessage(Part messagePart, SimpleMessageFormat format) {
Part part;
if (format == SimpleMessageFormat.HTML) {
// HTML takes precedence, then text.
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/html");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, HTML found.");
}
return MessageExtractor.getTextFromPart(part);
}
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/plain");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, text found.");
}
String text = MessageExtractor.getTextFromPart(part);
return HtmlConverter.textToHtml(text);
}
} else if (format == SimpleMessageFormat.TEXT) {
// Text takes precedence, then html.
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/plain");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, text found.");
}
return MessageExtractor.getTextFromPart(part);
}
part = MimeUtility.findFirstPartByMimeType(messagePart, "text/html");
if (part != null) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, HTML found.");
}
String text = MessageExtractor.getTextFromPart(part);
return HtmlConverter.htmlToText(text);
}
}
// If we had nothing interesting, return an empty string.
return "";
}
public static String stripSignatureForHtmlMessage(String content) {
Matcher dashSignatureHtml = DASH_SIGNATURE_HTML.matcher(content);
if (dashSignatureHtml.find()) {
Matcher blockquoteStart = BLOCKQUOTE_START.matcher(content);
Matcher blockquoteEnd = BLOCKQUOTE_END.matcher(content);
List<Integer> start = new ArrayList<>();
List<Integer> end = new ArrayList<>();
while (blockquoteStart.find()) {
start.add(blockquoteStart.start());
}
while (blockquoteEnd.find()) {
end.add(blockquoteEnd.start());
}
if (start.size() != end.size()) {
Log.d(K9.LOG_TAG, "There are " + start.size() + " <blockquote> tags, but " +
end.size() + " </blockquote> tags. Refusing to strip.");
} else if (start.size() > 0) {
// Ignore quoted signatures in blockquotes.
dashSignatureHtml.region(0, start.get(0));
if (dashSignatureHtml.find()) {
// before first <blockquote>.
content = content.substring(0, dashSignatureHtml.start());
} else {
for (int i = 0; i < start.size() - 1; i++) {
// within blockquotes.
if (end.get(i) < start.get(i + 1)) {
dashSignatureHtml.region(end.get(i), start.get(i + 1));
if (dashSignatureHtml.find()) {
content = content.substring(0, dashSignatureHtml.start());
break;
}
}
}
if (end.get(end.size() - 1) < content.length()) {
// after last </blockquote>.
dashSignatureHtml.region(end.get(end.size() - 1), content.length());
if (dashSignatureHtml.find()) {
content = content.substring(0, dashSignatureHtml.start());
}
}
}
} else {
// No blockquotes found.
content = content.substring(0, dashSignatureHtml.start());
}
}
// Fix the stripping off of closing tags if a signature was stripped,
// as well as clean up the HTML of the quoted message.
HtmlCleaner cleaner = new HtmlCleaner();
CleanerProperties properties = cleaner.getProperties();
// see http://htmlcleaner.sourceforge.net/parameters.php for descriptions
properties.setNamespacesAware(false);
properties.setAdvancedXmlEscape(false);
properties.setOmitXmlDeclaration(true);
properties.setOmitDoctypeDeclaration(false);
properties.setTranslateSpecialEntities(false);
properties.setRecognizeUnicodeChars(false);
TagNode node = cleaner.clean(content);
SimpleHtmlSerializer htmlSerialized = new SimpleHtmlSerializer(properties);
content = htmlSerialized.getAsString(node, "UTF8");
return content;
}
public static String stripSignatureForTextMessage(String content) {
if (DASH_SIGNATURE_PLAIN.matcher(content).find()) {
content = DASH_SIGNATURE_PLAIN.matcher(content).replaceFirst("\r\n");
}
return content;
}
}

View file

@ -1,4 +1,4 @@
package com.fsck.k9.message;
package com.fsck.k9.message.quote;
import java.io.Serializable;

View file

@ -0,0 +1,36 @@
package com.fsck.k9.message.quote;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import android.content.res.Resources;
import com.fsck.k9.mail.Message;
class QuoteHelper {
// amount of extra buffer to allocate to accommodate quoting headers or prefixes
static final int QUOTE_BUFFER_LENGTH = 512;
/**
* Extract the date from a message and convert it into a locale-specific
* date string suitable for use in a header for a quoted message.
*
* @return A string with the formatted date/time
*/
static String getSentDateText(Resources resources, Message message) {
try {
final int dateStyle = DateFormat.LONG;
final int timeStyle = DateFormat.LONG;
Date date = message.getSentDate();
Locale locale = resources.getConfiguration().locale;
return DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale)
.format(date);
} catch (Exception e) {
return "";
}
}
}

View file

@ -0,0 +1,83 @@
package com.fsck.k9.message.quote;
import android.content.res.Resources;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.R;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import static com.fsck.k9.message.quote.QuoteHelper.QUOTE_BUFFER_LENGTH;
public class TextQuoteCreator {
private static final int REPLY_WRAP_LINE_WIDTH = 72;
/**
* Add quoting markup to a text message.
* @param originalMessage Metadata for message being quoted.
* @param messageBody Text of the message to be quoted.
* @param quoteStyle Style of quoting.
* @return Quoted text.
* @throws MessagingException
*/
public static String quoteOriginalTextMessage(Resources resources, Message originalMessage, String messageBody, QuoteStyle quoteStyle, String prefix) throws MessagingException {
String body = messageBody == null ? "" : messageBody;
String sentDate = QuoteHelper.getSentDateText(resources, originalMessage);
if (quoteStyle == QuoteStyle.PREFIX) {
StringBuilder quotedText = new StringBuilder(body.length() + QUOTE_BUFFER_LENGTH);
if (sentDate.length() != 0) {
quotedText.append(String.format(
resources.getString(R.string.message_compose_reply_header_fmt_with_date) + "\r\n",
sentDate,
Address.toString(originalMessage.getFrom())));
} else {
quotedText.append(String.format(
resources.getString(R.string.message_compose_reply_header_fmt) + "\r\n",
Address.toString(originalMessage.getFrom()))
);
}
final String wrappedText = Utility.wrap(body, REPLY_WRAP_LINE_WIDTH - prefix.length());
// "$" and "\" in the quote prefix have to be escaped for
// the replaceAll() invocation.
final String escapedPrefix = prefix.replaceAll("(\\\\|\\$)", "\\\\$1");
quotedText.append(wrappedText.replaceAll("(?m)^", escapedPrefix));
// TODO is this correct?
return quotedText.toString().replaceAll("\\\r", "");
} else if (quoteStyle == QuoteStyle.HEADER) {
StringBuilder quotedText = new StringBuilder(body.length() + QUOTE_BUFFER_LENGTH);
quotedText.append("\r\n");
quotedText.append(resources.getString(R.string.message_compose_quote_header_separator)).append("\r\n");
if (originalMessage.getFrom() != null && Address.toString(originalMessage.getFrom()).length() != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_from)).append(" ").append(Address.toString(originalMessage.getFrom())).append("\r\n");
}
if (sentDate.length() != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_send_date)).append(" ").append(sentDate).append("\r\n");
}
if (originalMessage.getRecipients(RecipientType.TO) != null && originalMessage.getRecipients(RecipientType.TO).length != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_to)).append(" ").append(Address.toString(originalMessage.getRecipients(RecipientType.TO))).append("\r\n");
}
if (originalMessage.getRecipients(RecipientType.CC) != null && originalMessage.getRecipients(RecipientType.CC).length != 0) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_cc)).append(" ").append(Address.toString(originalMessage.getRecipients(RecipientType.CC))).append("\r\n");
}
if (originalMessage.getSubject() != null) {
quotedText.append(resources.getString(R.string.message_compose_quote_header_subject)).append(" ").append(originalMessage.getSubject()).append("\r\n");
}
quotedText.append("\r\n");
quotedText.append(body);
return quotedText.toString();
} else {
// Shouldn't ever happen.
return body;
}
}
}

View file

@ -0,0 +1,90 @@
package com.fsck.k9.message.signature;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.util.Log;
import com.fsck.k9.K9;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer;
import org.htmlcleaner.TagNode;
public class HtmlSignatureRemover {
private static final Pattern DASH_SIGNATURE_HTML = Pattern.compile("(<br( /)?>|\r?\n)-- <br( /)?>", Pattern.CASE_INSENSITIVE);
private static final Pattern BLOCKQUOTE_START = Pattern.compile("<blockquote", Pattern.CASE_INSENSITIVE);
private static final Pattern BLOCKQUOTE_END = Pattern.compile("</blockquote>", Pattern.CASE_INSENSITIVE);
public static String stripSignature(String content) {
Matcher dashSignatureHtml = DASH_SIGNATURE_HTML.matcher(content);
if (dashSignatureHtml.find()) {
Matcher blockquoteStart = BLOCKQUOTE_START.matcher(content);
Matcher blockquoteEnd = BLOCKQUOTE_END.matcher(content);
List<Integer> start = new ArrayList<>();
List<Integer> end = new ArrayList<>();
while (blockquoteStart.find()) {
start.add(blockquoteStart.start());
}
while (blockquoteEnd.find()) {
end.add(blockquoteEnd.start());
}
if (start.size() != end.size()) {
Log.d(K9.LOG_TAG, "There are " + start.size() + " <blockquote> tags, but " +
end.size() + " </blockquote> tags. Refusing to strip.");
} else if (start.size() > 0) {
// Ignore quoted signatures in blockquotes.
dashSignatureHtml.region(0, start.get(0));
if (dashSignatureHtml.find()) {
// before first <blockquote>.
content = content.substring(0, dashSignatureHtml.start());
} else {
for (int i = 0; i < start.size() - 1; i++) {
// within blockquotes.
if (end.get(i) < start.get(i + 1)) {
dashSignatureHtml.region(end.get(i), start.get(i + 1));
if (dashSignatureHtml.find()) {
content = content.substring(0, dashSignatureHtml.start());
break;
}
}
}
if (end.get(end.size() - 1) < content.length()) {
// after last </blockquote>.
dashSignatureHtml.region(end.get(end.size() - 1), content.length());
if (dashSignatureHtml.find()) {
content = content.substring(0, dashSignatureHtml.start());
}
}
}
} else {
// No blockquotes found.
content = content.substring(0, dashSignatureHtml.start());
}
}
// Fix the stripping off of closing tags if a signature was stripped,
// as well as clean up the HTML of the quoted message.
HtmlCleaner cleaner = new HtmlCleaner();
CleanerProperties properties = cleaner.getProperties();
// see http://htmlcleaner.sourceforge.net/parameters.php for descriptions
properties.setNamespacesAware(false);
properties.setAdvancedXmlEscape(false);
properties.setOmitXmlDeclaration(true);
properties.setOmitDoctypeDeclaration(false);
properties.setTranslateSpecialEntities(false);
properties.setRecognizeUnicodeChars(false);
TagNode node = cleaner.clean(content);
SimpleHtmlSerializer htmlSerialized = new SimpleHtmlSerializer(properties);
content = htmlSerialized.getAsString(node, "UTF8");
return content;
}
}

View file

@ -0,0 +1,17 @@
package com.fsck.k9.message.signature;
import java.util.regex.Pattern;
public class TextSignatureRemover {
private static final Pattern DASH_SIGNATURE_PLAIN = Pattern.compile("\r\n-- \r\n.*", Pattern.DOTALL);
public static String stripSignature(String content) {
if (DASH_SIGNATURE_PLAIN.matcher(content).find()) {
content = DASH_SIGNATURE_PLAIN.matcher(content).replaceFirst("\r\n");
}
return content;
}
}

View file

@ -12,7 +12,7 @@ import android.widget.ImageButton;
import com.fsck.k9.FontSizes;
import com.fsck.k9.R;
import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.mailstore.AttachmentResolver;
import com.fsck.k9.message.QuotedTextMode;
import com.fsck.k9.message.SimpleMessageFormat;

View file

@ -13,8 +13,11 @@ import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.K9;
import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.activity.MessageCompose.Action;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.helper.QuotedMessageHelper;
import com.fsck.k9.message.extractors.BodyTextExtractor;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.message.quote.HtmlQuoteCreator;
import com.fsck.k9.message.quote.TextQuoteCreator;
import com.fsck.k9.message.signature.HtmlSignatureRemover;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
@ -22,10 +25,11 @@ import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.AttachmentResolver;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.message.IdentityField;
import com.fsck.k9.message.InsertableHtmlContent;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import com.fsck.k9.message.MessageBuilder;
import com.fsck.k9.message.QuotedTextMode;
import com.fsck.k9.message.SimpleMessageFormat;
import com.fsck.k9.message.signature.TextSignatureRemover;
public class QuotedMessagePresenter {
@ -98,17 +102,17 @@ public class QuotedMessagePresenter {
// Handle the original message in the reply
// If we already have sourceMessageBody, use that. It's pre-populated if we've got crypto going on.
String content = QuotedMessageHelper.getBodyTextFromMessage(messageViewInfo.rootPart, quotedTextFormat);
String content = BodyTextExtractor.getBodyTextFromMessage(messageViewInfo.rootPart, quotedTextFormat);
if (quotedTextFormat == SimpleMessageFormat.HTML) {
// Strip signature.
// closing tags such as </div>, </span>, </table>, </pre> will be cut off.
if (account.isStripSignature() && (action == Action.REPLY || action == Action.REPLY_ALL)) {
content = QuotedMessageHelper.stripSignatureForHtmlMessage(content);
content = HtmlSignatureRemover.stripSignature(content);
}
// Add the HTML reply header to the top of the content.
quotedHtmlContent = QuotedMessageHelper.quoteOriginalHtmlMessage(
quotedHtmlContent = HtmlQuoteCreator.quoteOriginalHtmlMessage(
resources, messageViewInfo.message, content, quoteStyle);
// Load the message with the reply header. TODO replace with MessageViewInfo data
@ -116,16 +120,16 @@ public class QuotedMessagePresenter {
AttachmentResolver.createFromPart(messageViewInfo.rootPart));
// TODO: Also strip the signature from the text/plain part
view.setQuotedText(QuotedMessageHelper.quoteOriginalTextMessage(resources, messageViewInfo.message,
QuotedMessageHelper.getBodyTextFromMessage(messageViewInfo.rootPart, SimpleMessageFormat.TEXT),
view.setQuotedText(TextQuoteCreator.quoteOriginalTextMessage(resources, messageViewInfo.message,
BodyTextExtractor.getBodyTextFromMessage(messageViewInfo.rootPart, SimpleMessageFormat.TEXT),
quoteStyle, account.getQuotePrefix()));
} else if (quotedTextFormat == SimpleMessageFormat.TEXT) {
if (account.isStripSignature() && (action == Action.REPLY || action == Action.REPLY_ALL)) {
content = QuotedMessageHelper.stripSignatureForTextMessage(content);
content = TextSignatureRemover.stripSignature(content);
}
view.setQuotedText(QuotedMessageHelper.quoteOriginalTextMessage(
view.setQuotedText(TextQuoteCreator.quoteOriginalTextMessage(
resources, messageViewInfo.message, content, quoteStyle, account.getQuotePrefix()));
}
@ -234,7 +238,7 @@ public class QuotedMessagePresenter {
// composition window. If that's the case, try and convert it to text to
// match the behavior in text mode.
view.setMessageContentCharacters(
QuotedMessageHelper.getBodyTextFromMessage(messageViewInfo.message, SimpleMessageFormat.TEXT));
BodyTextExtractor.getBodyTextFromMessage(messageViewInfo.message, SimpleMessageFormat.TEXT));
forcePlainText = true;
showOrHideQuotedText(quotedMode);

View file

@ -30,7 +30,7 @@ import android.widget.Toast;
import com.fsck.k9.R;
import com.fsck.k9.helper.ClipboardManager;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.message.html.HtmlConverter;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mailstore.AttachmentResolver;

View file

@ -17,7 +17,6 @@ import android.widget.Toast;
import com.fsck.k9.K9;
import com.fsck.k9.K9.Theme;
import com.fsck.k9.R;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.mailstore.AttachmentResolver;

View file

@ -12,8 +12,8 @@ import android.app.Application;
import com.fsck.k9.GlobalsHelper;
import com.fsck.k9.activity.K9ActivityCommon;
import com.fsck.k9.helper.HtmlSanitizer;
import com.fsck.k9.helper.HtmlSanitizerHelper;
import com.fsck.k9.message.html.HtmlSanitizer;
import com.fsck.k9.message.html.HtmlSanitizerHelper;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;

View file

@ -26,6 +26,7 @@ import com.fsck.k9.mail.internet.MessageIdGenerator;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.message.MessageBuilder.Callback;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

View file

@ -33,6 +33,7 @@ import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.message.MessageBuilder.Callback;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import org.apache.commons.io.Charsets;

View file

@ -3,6 +3,7 @@ package com.fsck.k9.message;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import org.junit.Ignore;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;

View file

@ -1,4 +1,4 @@
package com.fsck.k9.helper;
package com.fsck.k9.message.html;
import java.io.BufferedWriter;
@ -6,6 +6,7 @@ import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import com.fsck.k9.message.html.HtmlConverter;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.junit.runner.RunWith;

View file

@ -0,0 +1,12 @@
package com.fsck.k9.message.html;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
public class HtmlHelper {
public static String extractText(String html) {
return Jsoup.clean(html, Whitelist.none());
}
}

View file

@ -1,4 +1,7 @@
package com.fsck.k9.helper;
package com.fsck.k9.message.html;
import com.fsck.k9.message.html.HtmlSanitizer;
public class HtmlSanitizerHelper {

View file

@ -1,4 +1,4 @@
package com.fsck.k9.helper;
package com.fsck.k9.message.html;
import org.junit.Before;

View file

@ -0,0 +1,148 @@
package com.fsck.k9.message.signature;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static com.fsck.k9.message.html.HtmlHelper.extractText;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 21)
public class HtmlSignatureRemoverTest {
@Test
public void shouldStripSignatureFromK9StyleHtml() throws Exception {
String html = "This is the body text" +
"<br>" +
"-- <br>" +
"Sent from my Android device with K-9 Mail. Please excuse my brevity.";
String withoutSignature = HtmlSignatureRemover.stripSignature(html);
assertEquals("This is the body text", extractText(withoutSignature));
}
@Ignore
@Test
public void shouldStripSignatureFromThunderbirdStyleHtml() throws Exception {
String html = "<html>\r\n" +
" <head>\r\n" +
" <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\r\n" +
" </head>\r\n" +
" <body bgcolor=\"#FFFFFF\" text=\"#000000\">\r\n" +
" <p>This is the body text<br>\r\n" +
" </p>\r\n" +
" -- <br>\r\n" +
" <div class=\"moz-signature\">Sent from my Android device with K-9 Mail." +
" Please excuse my brevity.</div>\r\n" +
" </body>\r\n" +
"</html>";
String withoutSignature = HtmlSignatureRemover.stripSignature(html);
assertEquals("This is the body text", extractText(withoutSignature));
}
@Test
public void shouldStripSignatureBeforeBlockquoteTag() throws Exception {
String html = "<html><head></head><body>" +
"<div>" +
"This is the body text" +
"<br>" +
"-- <br>" +
"<blockquote>" +
"Sent from my Android device with K-9 Mail. Please excuse my brevity." +
"</blockquote>" +
"</div>" +
"</body></html>";
String withoutSignature = HtmlSignatureRemover.stripSignature(html);
assertEquals("<html><head></head><body>" +
"<div>This is the body text</div>" +
"</body></html>",
withoutSignature);
}
@Test
public void shouldNotStripSignatureInsideBlockquoteTags() throws Exception {
String html = "<html><head></head><body>" +
"<blockquote>" +
"This is some quoted text" +
"<br>" +
"-- <br>" +
"Inner signature" +
"</blockquote>" +
"<div>" +
"This is the body text" +
"</div>" +
"</body></html>";
String withoutSignature = HtmlSignatureRemover.stripSignature(html);
assertEquals("<html><head></head><body>" +
"<blockquote>" +
"This is some quoted text" +
"<br />" +
"-- <br />" +
"Inner signature" +
"</blockquote>" +
"<div>This is the body text</div>" +
"</body></html>",
withoutSignature);
}
@Test
public void shouldStripSignatureBetweenBlockquoteTags() throws Exception {
String html = "<html><head></head><body>" +
"<blockquote>" +
"Some quote" +
"</blockquote>" +
"<div>" +
"This is the body text" +
"<br>" +
"-- <br>" +
"<blockquote>" +
"Sent from my Android device with K-9 Mail. Please excuse my brevity." +
"</blockquote>" +
"<br>" +
"-- <br>" +
"Signature inside signature" +
"</div>" +
"</body></html>";
String withoutSignature = HtmlSignatureRemover.stripSignature(html);
assertEquals("<html><head></head><body>" +
"<blockquote>Some quote</blockquote>" +
"<div>This is the body text</div>" +
"</body></html>",
withoutSignature);
}
@Test
public void shouldStripSignatureAfterLastBlockquoteTags() throws Exception {
String html = "<html><head></head><body>" +
"This is the body text" +
"<br>" +
"<blockquote>" +
"Some quote" +
"</blockquote>" +
"<br>" +
"-- <br>" +
"Sent from my Android device with K-9 Mail. Please excuse my brevity." +
"</body></html>";
String withoutSignature = HtmlSignatureRemover.stripSignature(html);
assertEquals("<html><head></head><body>" +
"This is the body text<br />" +
"<blockquote>Some quote</blockquote>" +
"</body></html>",
withoutSignature);
}
}

View file

@ -0,0 +1,22 @@
package com.fsck.k9.message.signature;
import com.fsck.k9.message.signature.TextSignatureRemover;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TextSignatureRemoverTest {
@Test
public void shouldStripSignature() throws Exception {
String text = "This is the body text\r\n" +
"\r\n" +
"-- \r\n" +
"Sent from my Android device with K-9 Mail. Please excuse my brevity.";
String withoutSignature = TextSignatureRemover.stripSignature(text);
assertEquals("This is the body text\r\n\r\n", withoutSignature);
}
}