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'); } }