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    }