001 /****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one *
003 * or more contributor license agreements. See the NOTICE file *
004 * distributed with this work for additional information *
005 * regarding copyright ownership. The ASF licenses this file *
006 * to you under the Apache License, Version 2.0 (the *
007 * "License"); you may not use this file except in compliance *
008 * with the License. You may obtain a copy of the License at *
009 * *
010 * http://www.apache.org/licenses/LICENSE-2.0 *
011 * *
012 * Unless required by applicable law or agreed to in writing, *
013 * software distributed under the License is distributed on an *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015 * KIND, either express or implied. See the License for the *
016 * specific language governing permissions and limitations *
017 * under the License. *
018 ****************************************************************/
019
020 package org.apache.mailet.base;
021
022 import javax.mail.Message;
023 import javax.mail.MessagingException;
024 import javax.mail.internet.ContentType;
025
026 import java.io.IOException;
027
028 /**
029 * <p>Manages texts encoded as <code>text/plain; format=flowed</code>.</p>
030 * <p>As a reference see:</p>
031 * <ul>
032 * <li><a href='http://www.rfc-editor.org/rfc/rfc2646.txt'>RFC2646</a></li>
033 * <li><a href='http://www.rfc-editor.org/rfc/rfc3676.txt'>RFC3676</a> (new method with DelSP support).
034 * </ul>
035 * <h4>Note</h4>
036 * <ul>
037 * <li>In order to decode, the input text must belong to a mail with headers similar to:
038 * Content-Type: text/plain; charset="CHARSET"; [delsp="yes|no"; ]format="flowed"
039 * (the quotes around CHARSET are not mandatory).
040 * Furthermore the header Content-Transfer-Encoding MUST NOT BE Quoted-Printable
041 * (see RFC3676 paragraph 4.2).(In fact this happens often for non 7bit messages).
042 * </li>
043 * <li>When encoding the input text will be changed eliminating every space found before CRLF,
044 * otherwise it won't be possible to recognize hard breaks from soft breaks.
045 * In this scenario encoding and decoding a message will not return a message identical to
046 * the original (lines with hard breaks will be trimmed)
047 * </li>
048 * </ul>
049 */
050 public final class FlowedMessageUtils {
051 public static final char RFC2646_SPACE = ' ';
052 public static final char RFC2646_QUOTE = '>';
053 public static final String RFC2646_SIGNATURE = "-- ";
054 public static final String RFC2646_CRLF = "\r\n";
055 public static final String RFC2646_FROM = "From ";
056 public static final int RFC2646_WIDTH = 78;
057
058 private FlowedMessageUtils() {
059 // this class cannot be instantiated
060 }
061
062 /**
063 * Decodes a text previously wrapped using "format=flowed".
064 */
065 public static String deflow(String text, boolean delSp) {
066 String[] lines = text.split("\r\n|\n", -1);
067 StringBuffer result = null;
068 StringBuffer resultLine = new StringBuffer();
069 int resultLineQuoteDepth = 0;
070 boolean resultLineFlowed = false;
071 // One more cycle, to close the last line
072 for (int i = 0; i <= lines.length; i++) {
073 String line = i < lines.length ? lines[i] : null;
074 int actualQuoteDepth = 0;
075
076 if (line != null && line.length() > 0) {
077 if (line.equals(RFC2646_SIGNATURE))
078 // signature handling (the previous line is not flowed)
079 resultLineFlowed = false;
080
081 else if (line.charAt(0) == RFC2646_QUOTE) {
082 // Quote
083 actualQuoteDepth = 1;
084 while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) actualQuoteDepth ++;
085 // if quote-depth changes wrt the previous line then this is not flowed
086 if (resultLineQuoteDepth != actualQuoteDepth) resultLineFlowed = false;
087 line = line.substring(actualQuoteDepth);
088
089 } else {
090 // id quote-depth changes wrt the first line then this is not flowed
091 if (resultLineQuoteDepth > 0) resultLineFlowed = false;
092 }
093
094 if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE)
095 // Line space-stuffed
096 line = line.substring(1);
097
098 // if the previous was the last then it was not flowed
099 } else if (line == null) resultLineFlowed = false;
100
101 // Add the PREVIOUS line.
102 // This often will find the flow looking for a space as the last char of the line.
103 // With quote changes or signatures it could be the followinf line to void the flow.
104 if (!resultLineFlowed && i > 0) {
105 if (resultLineQuoteDepth > 0) resultLine.insert(0, RFC2646_SPACE);
106 for (int j = 0; j < resultLineQuoteDepth; j++) resultLine.insert(0, RFC2646_QUOTE);
107 if (result == null) result = new StringBuffer();
108 else result.append(RFC2646_CRLF);
109 result.append(resultLine.toString());
110 resultLine = new StringBuffer();
111 resultLineFlowed = false;
112 }
113 resultLineQuoteDepth = actualQuoteDepth;
114
115 if (line != null) {
116 if (!line.equals(RFC2646_SIGNATURE) && line.endsWith("" + RFC2646_SPACE) && i < lines.length - 1) {
117 // Line flowed (NOTE: for the split operation the line having i == lines.length is the last that does not end with RFC2646_CRLF)
118 if (delSp) line = line.substring(0, line.length() - 1);
119 resultLineFlowed = true;
120 }
121
122 else resultLineFlowed = false;
123
124 resultLine.append(line);
125 }
126 }
127
128 return result.toString();
129 }
130
131 /**
132 * Obtains the content of the encoded message, if previously encoded as <code>format=flowed</code>.
133 */
134 public static String deflow(Message m) throws IOException, MessagingException {
135 ContentType ct = new ContentType(m.getContentType());
136 String format = ct.getParameter("format");
137 if (ct.getBaseType().equals("text/plain") && format != null && format.equalsIgnoreCase("flowed")) {
138 String delSp = ct.getParameter("delsp");
139 return deflow((String) m.getContent(), delSp != null && delSp.equalsIgnoreCase("yes"));
140
141 } else if (ct.getPrimaryType().equals("text")) return (String) m.getContent();
142
143 else return null;
144 }
145
146 /**
147 * If the message is <code>format=flowed</code>
148 * set the encoded version as message content.
149 */
150 public static void deflowMessage(Message m) throws MessagingException, IOException {
151 ContentType ct = new ContentType(m.getContentType());
152 String format = ct.getParameter("format");
153 if (ct.getBaseType().equals("text/plain") && format != null && format.equalsIgnoreCase("flowed")) {
154 String delSp = ct.getParameter("delsp");
155 String deflowed = deflow((String) m.getContent(), delSp != null && delSp.equalsIgnoreCase("yes"));
156
157 ct.getParameterList().remove("format");
158 ct.getParameterList().remove("delsp");
159
160 if (ct.toString().contains("flowed"))
161 System.out.println("\n\n*************************\n* ERROR!!! FlowedMessageUtils dind't remove the flowed correctly!\n******************\n\n" + ct.toString() + " \n " + ct.toString() + "\n");
162
163 m.setContent(deflowed, ct.toString());
164 m.saveChanges();
165 }
166 }
167
168
169 /**
170 * Encodes a text (using standard with).
171 */
172 public static String flow(String text, boolean delSp) {
173 return flow(text, delSp, RFC2646_WIDTH);
174 }
175
176 /**
177 * Decodes a text.
178 */
179 public static String flow(String text, boolean delSp, int width) {
180 StringBuilder result = new StringBuilder();
181 String[] lines = text.split("\r\n|\n", -1);
182 for (int i = 0; i < lines.length; i ++) {
183 String line = lines[i];
184 boolean notempty = line.length() > 0;
185
186 int quoteDepth = 0;
187 while (quoteDepth < line.length() && line.charAt(quoteDepth) == RFC2646_QUOTE) quoteDepth ++;
188 if (quoteDepth > 0) {
189 if (quoteDepth + 1 < line.length() && line.charAt(quoteDepth) == RFC2646_SPACE) line = line.substring(quoteDepth + 1);
190 else line = line.substring(quoteDepth);
191 }
192
193 while (notempty) {
194 int extra = 0;
195 if (quoteDepth == 0) {
196 if (line.startsWith("" + RFC2646_SPACE) || line.startsWith("" + RFC2646_QUOTE) || line.startsWith(RFC2646_FROM)) {
197 line = "" + RFC2646_SPACE + line;
198 extra = 1;
199 }
200 } else {
201 line = RFC2646_SPACE + line;
202 for (int j = 0; j < quoteDepth; j++) line = "" + RFC2646_QUOTE + line;
203 extra = quoteDepth + 1;
204 }
205
206 int j = width - 1;
207 if (j >= line.length()) j = line.length() - 1;
208 else {
209 while (j >= extra && ((delSp && isAlphaChar(text, j)) || (!delSp && line.charAt(j) != RFC2646_SPACE))) j --;
210 if (j < extra) {
211 // Not able to cut a word: skip to word end even if greater than the max width
212 j = width - 1;
213 while (j < line.length() - 1 && ((delSp && isAlphaChar(text, j)) || (!delSp && line.charAt(j) != RFC2646_SPACE))) j ++;
214 }
215 }
216
217 result.append(line.substring(0, j + 1));
218 if (j < line.length() - 1) {
219 if (delSp) result.append(RFC2646_SPACE);
220 result.append(RFC2646_CRLF);
221 }
222
223 line = line.substring(j + 1);
224 notempty = line.length() > 0;
225 }
226
227 if (i < lines.length - 1) {
228 // NOTE: Have to trim the spaces before, otherwise it won't recognize soft-break from hard break.
229 // Deflow of flowed message will not be identical to the original.
230 while (result.length() > 0 && result.charAt(result.length() - 1) == RFC2646_SPACE) result.deleteCharAt(result.length() - 1);
231 result.append(RFC2646_CRLF);
232 }
233 }
234
235 return result.toString();
236 }
237
238 /**
239 * Encodes the input text and sets it as the new message content.
240 */
241 public static void setFlowedContent(Message m, String text, boolean delSp) throws MessagingException {
242 setFlowedContent(m, text, delSp, RFC2646_WIDTH, true, null);
243 }
244
245 /**
246 * Encodes the input text and sets it as the new message content.
247 */
248 public static void setFlowedContent(Message m, String text, boolean delSp, int width, boolean preserveCharset, String charset) throws MessagingException {
249 String coded = flow(text, delSp, width);
250 if (preserveCharset) {
251 ContentType ct = new ContentType(m.getContentType());
252 charset = ct.getParameter("charset");
253 }
254 ContentType ct = new ContentType();
255 ct.setPrimaryType("text");
256 ct.setSubType("plain");
257 if (charset != null) ct.setParameter("charset", charset);
258 ct.setParameter("format", "flowed");
259 if (delSp) ct.setParameter("delsp", "yes");
260 m.setContent(coded, ct.toString());
261 m.saveChanges();
262 }
263
264 /**
265 * Encodes the message content (if text/plain).
266 */
267 public static void flowMessage(Message m, boolean delSp) throws MessagingException, IOException {
268 flowMessage(m, delSp, RFC2646_WIDTH);
269 }
270
271 /**
272 * Encodes the message content (if text/plain).
273 */
274 public static void flowMessage(Message m, boolean delSp, int width) throws MessagingException, IOException {
275 ContentType ct = new ContentType(m.getContentType());
276 if (!ct.getBaseType().equals("text/plain")) return;
277 String format = ct.getParameter("format");
278 String text = format != null && format.equals("flowed") ? deflow(m) : (String) m.getContent();
279 String coded = flow(text, delSp, width);
280 ct.setParameter("format", "flowed");
281 if (delSp) ct.setParameter("delsp", "yes");
282 m.setContent(coded, ct.toString());
283 m.saveChanges();
284 }
285
286 /**
287 * Checks whether the char is part of a word.
288 * <p>RFC assert a word cannot be splitted (even if the length is greater than the maximum length).
289 */
290 public static boolean isAlphaChar(String text, int index) {
291 // Note: a list of chars is available here:
292 // http://www.zvon.org/tmRFC/RFC2646/Output/index.html
293 char c = text.charAt(index);
294 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
295 }
296
297 /**
298 * Checks whether the input message is <code>format=flowed</code>.
299 */
300 public static boolean isFlowedTextMessage(Message m) throws MessagingException {
301 ContentType ct = new ContentType(m.getContentType());
302 String format = ct.getParameter("format");
303 return ct.getBaseType().equals("text/plain") && format != null && format.equalsIgnoreCase("flowed");
304 }
305 }