BodyDescriptor.java revision 8978aac1977408b05e386ae846c30920c7faa0a6
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
20package org.apache.james.mime4j;
21
22import java.util.HashMap;
23import java.util.Map;
24
25import org.apache.commons.logging.Log;
26import org.apache.commons.logging.LogFactory;
27
28/**
29 * Encapsulates the values of the MIME-specific header fields
30 * (which starts with <code>Content-</code>).
31 *
32 *
33 * @version $Id: BodyDescriptor.java,v 1.4 2005/02/11 10:08:37 ntherning Exp $
34 */
35public class BodyDescriptor {
36    private static Log log = LogFactory.getLog(BodyDescriptor.class);
37
38    private String mimeType = "text/plain";
39    private String boundary = null;
40    private String charset = "us-ascii";
41    private String transferEncoding = "7bit";
42    private Map parameters = new HashMap();
43    private boolean contentTypeSet = false;
44    private boolean contentTransferEncSet = false;
45
46    /**
47     * Creates a new root <code>BodyDescriptor</code> instance.
48     */
49    public BodyDescriptor() {
50        this(null);
51    }
52
53    /**
54     * Creates a new <code>BodyDescriptor</code> instance.
55     *
56     * @param parent the descriptor of the parent or <code>null</code> if this
57     *        is the root descriptor.
58     */
59    public BodyDescriptor(BodyDescriptor parent) {
60        if (parent != null && parent.isMimeType("multipart/digest")) {
61            mimeType = "message/rfc822";
62        } else {
63            mimeType = "text/plain";
64        }
65    }
66
67    /**
68     * Should be called for each <code>Content-</code> header field of
69     * a MIME message or part.
70     *
71     * @param name the field name.
72     * @param value the field value.
73     */
74    public void addField(String name, String value) {
75
76        name = name.trim().toLowerCase();
77
78        if (name.equals("content-transfer-encoding") && !contentTransferEncSet) {
79            contentTransferEncSet = true;
80
81            value = value.trim().toLowerCase();
82            if (value.length() > 0) {
83                transferEncoding = value;
84            }
85
86        } else if (name.equals("content-type") && !contentTypeSet) {
87            contentTypeSet = true;
88
89            value = value.trim();
90
91            /*
92             * Unfold Content-Type value
93             */
94            StringBuffer sb = new StringBuffer();
95            for (int i = 0; i < value.length(); i++) {
96                char c = value.charAt(i);
97                if (c == '\r' || c == '\n') {
98                    continue;
99                }
100                sb.append(c);
101            }
102
103            Map params = getHeaderParams(sb.toString());
104
105            String main = (String) params.get("");
106            if (main != null) {
107                main = main.toLowerCase().trim();
108                int index = main.indexOf('/');
109                boolean valid = false;
110                if (index != -1) {
111                    String type = main.substring(0, index).trim();
112                    String subtype = main.substring(index + 1).trim();
113                    if (type.length() > 0 && subtype.length() > 0) {
114                        main = type + "/" + subtype;
115                        valid = true;
116                    }
117                }
118
119                if (!valid) {
120                    main = null;
121                }
122            }
123            String b = (String) params.get("boundary");
124
125            if (main != null
126                    && ((main.startsWith("multipart/") && b != null)
127                            || !main.startsWith("multipart/"))) {
128
129                mimeType = main;
130            }
131
132            if (isMultipart()) {
133                boundary = b;
134            }
135
136            String c = (String) params.get("charset");
137            if (c != null) {
138                c = c.trim();
139                if (c.length() > 0) {
140                    charset = c.toLowerCase();
141                }
142            }
143
144            /*
145             * Add all other parameters to parameters.
146             */
147            parameters.putAll(params);
148            parameters.remove("");
149            parameters.remove("boundary");
150            parameters.remove("charset");
151        }
152    }
153
154    private Map getHeaderParams(String headerValue) {
155        Map result = new HashMap();
156
157        // split main value and parameters
158        String main;
159        String rest;
160        if (headerValue.indexOf(";") == -1) {
161            main = headerValue;
162            rest = null;
163        } else {
164            main = headerValue.substring(0, headerValue.indexOf(";"));
165            rest = headerValue.substring(main.length() + 1);
166        }
167
168        result.put("", main);
169        if (rest != null) {
170            char[] chars = rest.toCharArray();
171            StringBuffer paramName = new StringBuffer();
172            StringBuffer paramValue = new StringBuffer();
173
174            final byte READY_FOR_NAME = 0;
175            final byte IN_NAME = 1;
176            final byte READY_FOR_VALUE = 2;
177            final byte IN_VALUE = 3;
178            final byte IN_QUOTED_VALUE = 4;
179            final byte VALUE_DONE = 5;
180            final byte ERROR = 99;
181
182            byte state = READY_FOR_NAME;
183            boolean escaped = false;
184            for (int i = 0; i < chars.length; i++) {
185                char c = chars[i];
186
187                switch (state) {
188                    case ERROR:
189                        if (c == ';')
190                            state = READY_FOR_NAME;
191                        break;
192
193                    case READY_FOR_NAME:
194                        if (c == '=') {
195                            log.error("Expected header param name, got '='");
196                            state = ERROR;
197                            break;
198                        }
199
200                        paramName = new StringBuffer();
201                        paramValue = new StringBuffer();
202
203                        state = IN_NAME;
204                        // fall-through
205
206                    case IN_NAME:
207                        if (c == '=') {
208                            if (paramName.length() == 0)
209                                state = ERROR;
210                            else
211                                state = READY_FOR_VALUE;
212                            break;
213                        }
214
215                        // not '='... just add to name
216                        paramName.append(c);
217                        break;
218
219                    case READY_FOR_VALUE:
220                        boolean fallThrough = false;
221                        switch (c) {
222                            case ' ':
223                            case '\t':
224                                break;  // ignore spaces, especially before '"'
225
226                            case '"':
227                                state = IN_QUOTED_VALUE;
228                                break;
229
230                            default:
231                                state = IN_VALUE;
232                                fallThrough = true;
233                                break;
234                        }
235                        if (!fallThrough)
236                            break;
237
238                        // fall-through
239
240                    case IN_VALUE:
241                        fallThrough = false;
242                        switch (c) {
243                            case ';':
244                            case ' ':
245                            case '\t':
246                                result.put(
247                                   paramName.toString().trim().toLowerCase(),
248                                   paramValue.toString().trim());
249                                state = VALUE_DONE;
250                                fallThrough = true;
251                                break;
252                            default:
253                                paramValue.append(c);
254                                break;
255                        }
256                        if (!fallThrough)
257                            break;
258
259                    case VALUE_DONE:
260                        switch (c) {
261                            case ';':
262                                state = READY_FOR_NAME;
263                                break;
264
265                            case ' ':
266                            case '\t':
267                                break;
268
269                            default:
270                                state = ERROR;
271                                break;
272                        }
273                        break;
274
275                    case IN_QUOTED_VALUE:
276                        switch (c) {
277                            case '"':
278                                if (!escaped) {
279                                    // don't trim quoted strings; the spaces could be intentional.
280                                    result.put(
281                                            paramName.toString().trim().toLowerCase(),
282                                            paramValue.toString());
283                                    state = VALUE_DONE;
284                                } else {
285                                    escaped = false;
286                                    paramValue.append(c);
287                                }
288                                break;
289
290                            case '\\':
291                                if (escaped) {
292                                    paramValue.append('\\');
293                                }
294                                escaped = !escaped;
295                                break;
296
297                            default:
298                                if (escaped) {
299                                    paramValue.append('\\');
300                                }
301                                escaped = false;
302                                paramValue.append(c);
303                                break;
304                        }
305                        break;
306
307                }
308            }
309
310            // done looping.  check if anything is left over.
311            if (state == IN_VALUE) {
312                result.put(
313                        paramName.toString().trim().toLowerCase(),
314                        paramValue.toString().trim());
315            }
316        }
317
318        return result;
319    }
320
321
322    public boolean isMimeType(String mimeType) {
323        return this.mimeType.equals(mimeType.toLowerCase());
324    }
325
326    /**
327     * Return true if the BodyDescriptor belongs to a message
328     *
329     * @return
330     */
331    public boolean isMessage() {
332        return mimeType.equals("message/rfc822");
333    }
334
335    /**
336     * Retrun true if the BodyDescripotro belogns to a multipart
337     *
338     * @return
339     */
340    public boolean isMultipart() {
341        return mimeType.startsWith("multipart/");
342    }
343
344    /**
345     * Return the MimeType
346     *
347     * @return mimeType
348     */
349    public String getMimeType() {
350        return mimeType;
351    }
352
353    /**
354     * Return the boundary
355     *
356     * @return boundary
357     */
358    public String getBoundary() {
359        return boundary;
360    }
361
362    /**
363     * Return the charset
364     *
365     * @return charset
366     */
367    public String getCharset() {
368        return charset;
369    }
370
371    /**
372     * Return all parameters for the BodyDescriptor
373     *
374     * @return parameters
375     */
376    public Map getParameters() {
377        return parameters;
378    }
379
380    /**
381     * Return the TransferEncoding
382     *
383     * @return transferEncoding
384     */
385    public String getTransferEncoding() {
386        return transferEncoding;
387    }
388
389    /**
390     * Return true if it's base64 encoded
391     *
392     * @return
393     *
394     */
395    public boolean isBase64Encoded() {
396        return "base64".equals(transferEncoding);
397    }
398
399    /**
400     * Return true if it's quoted-printable
401     * @return
402     */
403    public boolean isQuotedPrintableEncoded() {
404        return "quoted-printable".equals(transferEncoding);
405    }
406
407    public String toString() {
408        return mimeType;
409    }
410}
411