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