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.james.mime4j;
21 
22 import java.util.HashMap;
23 import 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  */
32 public 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      */
BodyDescriptor()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      */
BodyDescriptor(BodyDescriptor parent)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      */
addField(String name, String value)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 
getHeaderParams(String headerValue)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 
isMimeType(String mimeType)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      */
isMessage()328     public boolean isMessage() {
329         return mimeType.equals("message/rfc822");
330     }
331 
332     /**
333      * Return true if the BodyDescripotro belongs to a multipart
334      */
isMultipart()335     public boolean isMultipart() {
336         return mimeType.startsWith("multipart/");
337     }
338 
339     /**
340      * Return the MimeType
341      */
getMimeType()342     public String getMimeType() {
343         return mimeType;
344     }
345 
346     /**
347      * Return the boundary
348      */
getBoundary()349     public String getBoundary() {
350         return boundary;
351     }
352 
353     /**
354      * Return the charset
355      */
getCharset()356     public String getCharset() {
357         return charset;
358     }
359 
360     /**
361      * Return all parameters for the BodyDescriptor
362      */
getParameters()363     public Map<String, String> getParameters() {
364         return parameters;
365     }
366 
367     /**
368      * Return the TransferEncoding
369      */
getTransferEncoding()370     public String getTransferEncoding() {
371         return transferEncoding;
372     }
373 
374     /**
375      * Return true if it's base64 encoded
376      */
isBase64Encoded()377     public boolean isBase64Encoded() {
378         return "base64".equals(transferEncoding);
379     }
380 
381     /**
382      * Return true if it's quoted-printable
383      */
isQuotedPrintableEncoded()384     public boolean isQuotedPrintableEncoded() {
385         return "quoted-printable".equals(transferEncoding);
386     }
387 
388     @Override
toString()389     public String toString() {
390         return mimeType;
391     }
392 }
393