View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.mailet.base;
21  
22  import javax.mail.Message;
23  import javax.mail.MessagingException;
24  import javax.mail.internet.ContentType;
25  
26  import java.io.IOException;
27  
28  /**
29   * <p>Manages texts encoded as <code>text/plain; format=flowed</code>.</p>
30   * <p>As a reference see:</p>
31   * <ul>
32   * <li><a href='http://www.rfc-editor.org/rfc/rfc2646.txt'>RFC2646</a></li>
33   * <li><a href='http://www.rfc-editor.org/rfc/rfc3676.txt'>RFC3676</a> (new method with DelSP support).
34   * </ul>
35   * <h4>Note</h4>
36   * <ul>
37   * <li>In order to decode, the input text must belong to a mail with headers similar to:
38   *   Content-Type: text/plain; charset="CHARSET"; [delsp="yes|no"; ]format="flowed"
39   *   (the quotes around CHARSET are not mandatory).
40   *   Furthermore the header Content-Transfer-Encoding MUST NOT BE Quoted-Printable
41   *   (see RFC3676 paragraph 4.2).(In fact this happens often for non 7bit messages).
42   * </li>
43   * <li>When encoding the input text will be changed eliminating every space found before CRLF,
44   *   otherwise it won't be possible to recognize hard breaks from soft breaks.
45   *   In this scenario encoding and decoding a message will not return a message identical to 
46   *   the original (lines with hard breaks will be trimmed)
47   * </li>
48   * </ul>
49   */
50  public final class FlowedMessageUtils {
51      public static final char RFC2646_SPACE = ' ';
52      public static final char RFC2646_QUOTE = '>';
53      public static final String RFC2646_SIGNATURE = "-- ";
54      public static final String RFC2646_CRLF = "\r\n";
55      public static final String RFC2646_FROM = "From ";
56      public static final int RFC2646_WIDTH = 78;
57      
58      private FlowedMessageUtils() {
59          // this class cannot be instantiated
60      }
61      
62      /**
63       * Decodes a text previously wrapped using "format=flowed".
64       */
65      public static String deflow(String text, boolean delSp) {
66          String[] lines = text.split("\r\n|\n", -1);
67          StringBuffer result = null;
68          StringBuffer resultLine = new StringBuffer();
69          int resultLineQuoteDepth = 0;
70          boolean resultLineFlowed = false;
71          // One more cycle, to close the last line
72          for (int i = 0; i <= lines.length; i++) {
73              String line = i < lines.length ? lines[i] : null;
74              int actualQuoteDepth = 0;
75              
76              if (line != null && line.length() > 0) {
77                  if (line.equals(RFC2646_SIGNATURE))
78                      // signature handling (the previous line is not flowed)
79                      resultLineFlowed = false;
80                  
81                  else if (line.charAt(0) == RFC2646_QUOTE) {
82                      // Quote
83                      actualQuoteDepth = 1;
84                      while (actualQuoteDepth < line.length() && line.charAt(actualQuoteDepth) == RFC2646_QUOTE) actualQuoteDepth ++;
85                      // if quote-depth changes wrt the previous line then this is not flowed
86                      if (resultLineQuoteDepth != actualQuoteDepth) resultLineFlowed = false;
87                      line = line.substring(actualQuoteDepth);
88                      
89                  } else {
90                      // id quote-depth changes wrt the first line then this is not flowed
91                      if (resultLineQuoteDepth > 0) resultLineFlowed = false;
92                  }
93                      
94                  if (line.length() > 0 && line.charAt(0) == RFC2646_SPACE)
95                      // Line space-stuffed
96                      line = line.substring(1);
97                  
98              // if the previous was the last then it was not flowed
99              } 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().indexOf("flowed") >= 0) 
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         StringBuffer result = new StringBuffer();
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 }