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