package com.fsck.k9.message.quote;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.res.Resources;
import timber.log.Timber;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.K9;
import com.fsck.k9.R;
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.message.html.HtmlConverter;
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+[^>]*>)).*)");
private static final Pattern FIND_INSERTION_POINT_HEAD = Pattern.compile("(?si:.*?(<head(?:>|\\s+[^>]*>)).*)");
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>).*?)");
// 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;
// HTML bits to insert as appropriate
// TODO is it safe to assume utf-8 here?
private static final String FIND_INSERTION_POINT_HTML_CONTENT = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\r\n<html>";
private static final String FIND_INSERTION_POINT_HTML_END_CONTENT = "</html>";
private static final String FIND_INSERTION_POINT_HEAD_CONTENT = "<head><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\"></head>";
// Index of the start of the beginning of a String.
private static final int FIND_INSERTION_POINT_START_OF_STRING = 0;
/**
* Add quoting markup to a HTML message.
* @param originalMessage Metadata for message being quoted.
* @param messageBody Text of the message to be quoted.
* @param quoteStyle Style of quoting.
* @return Modified insertable message.
* @throws MessagingException
*/
public static InsertableHtmlContent quoteOriginalHtmlMessage(Resources resources, Message originalMessage,
String messageBody, QuoteStyle quoteStyle) throws MessagingException {
InsertableHtmlContent insertable = findInsertionPoints(messageBody);
String sentDate = QuoteHelper.getSentDateText(resources, originalMessage);
String fromAddress = Address.toString(originalMessage.getFrom());
if (quoteStyle == QuoteStyle.PREFIX) {
StringBuilder header = new StringBuilder(QuoteHelper.QUOTE_BUFFER_LENGTH);
header.append("<div class=\"gmail_quote\">");
if (sentDate.length() != 0) {
header.append(HtmlConverter.textToHtmlFragment(String.format(
resources.getString(R.string.message_compose_reply_header_fmt_with_date), sentDate, fromAddress)
));
} else {
header.append(HtmlConverter.textToHtmlFragment(String.format(
resources.getString(R.string.message_compose_reply_header_fmt), fromAddress)
));
}
header.append("<blockquote class=\"gmail_quote\" " +
"style=\"margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;\">\r\n");
String footer = "</blockquote></div>";
insertable.insertIntoQuotedHeader(header.toString());
insertable.insertIntoQuotedFooter(footer);
} else if (quoteStyle == QuoteStyle.HEADER) {
StringBuilder header = new StringBuilder();
header.append("<div style='font-size:10.0pt;font-family:\"Tahoma\",\"sans-serif\";padding:3.0pt 0in 0in 0in'>\r\n");
header.append("<hr style='border:none;border-top:solid #E1E1E1 1.0pt'>\r\n"); // This gets converted into a horizontal line during html to text conversion.
if (originalMessage.getFrom() != null && fromAddress.length() != 0) {
header.append("<b>").append(resources.getString(R.string.message_compose_quote_header_from)).append("</b> ")
.append(HtmlConverter.textToHtmlFragment(fromAddress))
.append("<br>\r\n");
}
if (sentDate.length() != 0) {
header.append("<b>").append(resources.getString(R.string.message_compose_quote_header_send_date)).append("</b> ")
.append(sentDate)
.append("<br>\r\n");
}
if (originalMessage.getRecipients(RecipientType.TO) != null && originalMessage.getRecipients(RecipientType.TO).length != 0) {
header.append("<b>").append(resources.getString(R.string.message_compose_quote_header_to)).append("</b> ")
.append(HtmlConverter.textToHtmlFragment(Address.toString(originalMessage.getRecipients(RecipientType.TO))))
.append("<br>\r\n");
}
if (originalMessage.getRecipients(RecipientType.CC) != null && originalMessage.getRecipients(RecipientType.CC).length != 0) {
header.append("<b>").append(resources.getString(R.string.message_compose_quote_header_cc)).append("</b> ")
.append(HtmlConverter.textToHtmlFragment(Address.toString(originalMessage.getRecipients(RecipientType.CC))))
.append("<br>\r\n");
}
if (originalMessage.getSubject() != null) {
header.append("<b>").append(resources.getString(R.string.message_compose_quote_header_subject)).append("</b> ")
.append(HtmlConverter.textToHtmlFragment(originalMessage.getSubject()))
.append("<br>\r\n");
}
header.append("</div>\r\n");
header.append("<br>\r\n");
insertable.insertIntoQuotedHeader(header.toString());
}
return insertable;
}
/**
* <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
* contains both the insertion points and potentially modified HTML. The modified HTML should be
* used in place of the HTML in the original message.</p>
*
* <p>This method loosely mimics the HTML forward/reply behavior of BlackBerry OS 4.5/BIS 2.5,
* which in turn mimics Outlook 2003 (as best I can tell).</p>
*
* @param content Content to examine for HTML insertion points
* @return Insertion points and HTML to use for insertion.
*/
private static InsertableHtmlContent findInsertionPoints(final String content) {
InsertableHtmlContent insertable = new InsertableHtmlContent();
// If there is no content, don't bother doing any of the regex dancing.
if (content == null || content.equals("")) {
return insertable;
}
// Search for opening tags.
boolean hasHtmlTag = false;
boolean hasHeadTag = false;
boolean hasBodyTag = false;
// First see if we have an opening HTML tag. If we don't find one, we'll add one later.
Matcher htmlMatcher = FIND_INSERTION_POINT_HTML.matcher(content);
if (htmlMatcher.matches()) {
hasHtmlTag = true;
}
// Look for a HEAD tag. If we're missing a BODY tag, we'll use the close of the HEAD to start our content.
Matcher headMatcher = FIND_INSERTION_POINT_HEAD.matcher(content);
if (headMatcher.matches()) {
hasHeadTag = true;
}
// Look for a BODY tag. This is the ideal place for us to start our content.
Matcher bodyMatcher = FIND_INSERTION_POINT_BODY.matcher(content);
if (bodyMatcher.matches()) {
hasBodyTag = true;
}
Timber.d("Open: hasHtmlTag:%s hasHeadTag:%s hasBodyTag:%s", hasHtmlTag, hasHeadTag, hasBodyTag);
// Given our inspections, let's figure out where to start our content.
// This is the ideal case -- there's a BODY tag and we insert ourselves just after it.
if (hasBodyTag) {
insertable.setQuotedContent(new StringBuilder(content));
insertable.setHeaderInsertionPoint(bodyMatcher.end(FIND_INSERTION_POINT_FIRST_GROUP));
} else if (hasHeadTag) {
// Now search for a HEAD tag. We can insert after there.
// If BlackBerry sees a HEAD tag, it inserts right after that, so long as there is no BODY tag. It doesn't
// try to add BODY, either. Right or wrong, it seems to work fine.
insertable.setQuotedContent(new StringBuilder(content));
insertable.setHeaderInsertionPoint(headMatcher.end(FIND_INSERTION_POINT_FIRST_GROUP));
} else if (hasHtmlTag) {
// Lastly, check for an HTML tag.
// In this case, it will add a HEAD, but no BODY.
StringBuilder newContent = new StringBuilder(content);
// Insert the HEAD content just after the HTML tag.
newContent.insert(htmlMatcher.end(FIND_INSERTION_POINT_FIRST_GROUP), FIND_INSERTION_POINT_HEAD_CONTENT);
insertable.setQuotedContent(newContent);
// The new insertion point is the end of the HTML tag, plus the length of the HEAD content.
insertable.setHeaderInsertionPoint(htmlMatcher.end(FIND_INSERTION_POINT_FIRST_GROUP) + FIND_INSERTION_POINT_HEAD_CONTENT.length());
} else {
// If we have none of the above, we probably have a fragment of HTML. Yahoo! and Gmail both do this.
// Again, we add a HEAD, but not BODY.
StringBuilder newContent = new StringBuilder(content);
// Add the HTML and HEAD tags.
newContent.insert(FIND_INSERTION_POINT_START_OF_STRING, FIND_INSERTION_POINT_HEAD_CONTENT);
newContent.insert(FIND_INSERTION_POINT_START_OF_STRING, FIND_INSERTION_POINT_HTML_CONTENT);
// Append the </HTML> tag.
newContent.append(FIND_INSERTION_POINT_HTML_END_CONTENT);
insertable.setQuotedContent(newContent);
insertable.setHeaderInsertionPoint(FIND_INSERTION_POINT_HTML_CONTENT.length() + FIND_INSERTION_POINT_HEAD_CONTENT.length());
}
// Search for closing tags. We have to do this after we deal with opening tags since it may
// have modified the message.
boolean hasHtmlEndTag = false;
boolean hasBodyEndTag = false;
// First see if we have an opening HTML tag. If we don't find one, we'll add one later.
Matcher htmlEndMatcher = FIND_INSERTION_POINT_HTML_END.matcher(insertable.getQuotedContent());
if (htmlEndMatcher.matches()) {
hasHtmlEndTag = true;
}
// Look for a BODY tag. This is the ideal place for us to place our footer.
Matcher bodyEndMatcher = FIND_INSERTION_POINT_BODY_END.matcher(insertable.getQuotedContent());
if (bodyEndMatcher.matches()) {
hasBodyEndTag = true;
}
Timber.d("Close: hasHtmlEndTag:%s hasBodyEndTag:%s", hasHtmlEndTag, hasBodyEndTag);
// Now figure out where to put our footer.
// This is the ideal case -- there's a BODY tag and we insert ourselves just before it.
if (hasBodyEndTag) {
insertable.setFooterInsertionPoint(bodyEndMatcher.start(FIND_INSERTION_POINT_FIRST_GROUP));
} else if (hasHtmlEndTag) {
// Check for an HTML tag. Add ourselves just before it.
insertable.setFooterInsertionPoint(htmlEndMatcher.start(FIND_INSERTION_POINT_FIRST_GROUP));
} else {
// If we have none of the above, we probably have a fragment of HTML.
// Set our footer insertion point as the end of the string.
insertable.setFooterInsertionPoint(insertable.getQuotedContent().length());
}
return insertable;
}
}