package com.fsck.k9.mailstore.util;
/**
* Adapted from the Apache James project, see
* https://james.apache.org/mailet/base/apidocs/org/apache/mailet/base/FlowedMessageUtils.html
*
* <p>Manages texts encoded as <code>text/plain; format=flowed</code>.</p>
* <p>As a reference see:</p>
* <ul>
* <li><a href='http://www.rfc-editor.org/rfc/rfc2646.txt'>RFC2646</a></li>
* <li><a href='http://www.rfc-editor.org/rfc/rfc3676.txt'>RFC3676</a> (new method with DelSP support).
* </ul>
* <h4>Note</h4>
* <ul>
* <li>In order to decode, the input text must belong to a mail with headers similar to:
* Content-Type: text/plain; charset="CHARSET"; [delsp="yes|no"; ]format="flowed"
* (the quotes around CHARSET are not mandatory).
* Furthermore the header Content-Transfer-Encoding MUST NOT BE Quoted-Printable
* (see RFC3676 paragraph 4.2).(In fact this happens often for non 7bit messages).
* </li>
* <li>When encoding the input text will be changed eliminating every space found before CRLF,
* otherwise it won't be possible to recognize hard breaks from soft breaks.
* In this scenario encoding and decoding a message will not return a message identical to
* the original (lines with hard breaks will be trimmed)
* </li>
* </ul>
*/
public final class FlowedMessageUtils {
private static final char RFC2646_SPACE = ' ';
private static final char RFC2646_QUOTE = '>';
private static final String RFC2646_SIGNATURE = "-- ";
private static final String RFC2646_CRLF = "\r\n";
private static final String RFC2646_FROM = "From ";
private static final int RFC2646_WIDTH = 78;
private FlowedMessageUtils() {
// this class cannot be instantiated
}
/**
* Decodes a text previously wrapped using "format=flowed".
*/
public static String deflow(String text, boolean delSp) {
String[] lines = text.split("\r\n|\n", -1);
StringBuffer result = null;
StringBuffer resultLine = new StringBuffer();
int resultLineQuoteDepth = 0;
boolean resultLineFlowed = false;
// One more cycle, to close the last line
for (int i = 0; i <= lines.length; i++) {
String line = i < lines.length ? lines[i] : null;
int actualQuoteDepth = 0;
if (line != null && line.length() > 0) {
if (line.equals(RFC2646_SIGNATURE))
// signature handling (the previous line is not flowed)
resultLineFlowed = false;
else if (line.charAt(0) == RFC2646_QUOTE) {
// Quote
actualQuoteDepth = 1;
while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) actualQuoteDepth ++;
// if quote-depth changes wrt the previous line then this is not flowed
if (resultLineQuoteDepth != actualQuoteDepth) resultLineFlowed = false;
line = line.substring(actualQuoteDepth);
} else {
// id quote-depth changes wrt the first line then this is not flowed
if (resultLineQuoteDepth > 0) resultLineFlowed = false;
}
if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE)
// Line space-stuffed
line = line.substring(1);
// if the previous was the last then it was not flowed
} else if (line == null) resultLineFlowed = false;
// Add the PREVIOUS line.
// This often will find the flow looking for a space as the last char of the line.
// With quote changes or signatures it could be the followinf line to void the flow.
if (!resultLineFlowed && i > 0) {
if (resultLineQuoteDepth > 0) resultLine.insert(0, RFC2646_SPACE);
for (int j = 0; j < resultLineQuoteDepth; j++) resultLine.insert(0, RFC2646_QUOTE);
if (result == null) result = new StringBuffer();
else result.append(RFC2646_CRLF);
result.append(resultLine.toString());
resultLine = new StringBuffer();
resultLineFlowed = false;
}
resultLineQuoteDepth = actualQuoteDepth;
if (line != null) {
if (!line.equals(RFC2646_SIGNATURE) && line.endsWith("" + RFC2646_SPACE) && i < lines.length - 1) {
// Line flowed (NOTE: for the split operation the line having i == lines.length is the last that does not end with RFC2646_CRLF)
if (delSp) line = line.substring(0, line.length() - 1);
resultLineFlowed = true;
}
else resultLineFlowed = false;
resultLine.append(line);
}
}
return result.toString();
}
/**
* Encodes a text (using standard with).
*/
public static String flow(String text, boolean delSp) {
return flow(text, delSp, RFC2646_WIDTH);
}
/**
* Decodes a text.
*/
public static String flow(String text, boolean delSp, int width) {
StringBuilder result = new StringBuilder();
String[] lines = text.split("\r\n|\n", -1);
for (int i = 0; i < lines.length; i ++) {
String line = lines[i];
boolean notempty = line.length() > 0;
int quoteDepth = 0;
while (quoteDepth < line.length() && line.charAt(quoteDepth) == RFC2646_QUOTE) quoteDepth ++;
if (quoteDepth > 0) {
if (quoteDepth + 1 < line.length() && line.charAt(quoteDepth) == RFC2646_SPACE) line = line.substring(quoteDepth + 1);
else line = line.substring(quoteDepth);
}
while (notempty) {
int extra = 0;
if (quoteDepth == 0) {
if (line.startsWith("" + RFC2646_SPACE) || line.startsWith("" + RFC2646_QUOTE) || line.startsWith(RFC2646_FROM)) {
line = "" + RFC2646_SPACE + line;
extra = 1;
}
} else {
line = RFC2646_SPACE + line;
for (int j = 0; j < quoteDepth; j++) line = "" + RFC2646_QUOTE + line;
extra = quoteDepth + 1;
}
int j = width - 1;
if (j >= line.length()) j = line.length() - 1;
else {
while (j >= extra && ((delSp && isAlphaChar(text, j)) || (!delSp && line.charAt(j) != RFC2646_SPACE))) j --;
if (j < extra) {
// Not able to cut a word: skip to word end even if greater than the max width
j = width - 1;
while (j < line.length() - 1 && ((delSp && isAlphaChar(text, j)) || (!delSp && line.charAt(j) != RFC2646_SPACE))) j ++;
}
}
result.append(line.substring(0, j + 1));
if (j < line.length() - 1) {
if (delSp) result.append(RFC2646_SPACE);
result.append(RFC2646_CRLF);
}
line = line.substring(j + 1);
notempty = line.length() > 0;
}
if (i < lines.length - 1) {
// NOTE: Have to trim the spaces before, otherwise it won't recognize soft-break from hard break.
// Deflow of flowed message will not be identical to the original.
while (result.length() > 0 && result.charAt(result.length() - 1) == RFC2646_SPACE) result.deleteCharAt(result.length() - 1);
result.append(RFC2646_CRLF);
}
}
return result.toString();
}
/**
* Checks whether the char is part of a word.
* <p>RFC assert a word cannot be split (even if the length is greater than the maximum length).
*/
public static boolean isAlphaChar(String text, int index) {
// Note: a list of chars is available here:
// http://www.zvon.org/tmRFC/RFC2646/Output/index.html
char c = text.charAt(index);
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
}
}