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 parameters = new HashMap(); 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 params = getHeaderParams(sb.toString()); 101 102 String main = (String) 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 = (String) 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 = (String) 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 getHeaderParams(String headerValue) { 152 Map result = new HashMap(); 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 case VALUE_DONE: 257 switch (c) { 258 case ';': 259 state = READY_FOR_NAME; 260 break; 261 262 case ' ': 263 case '\t': 264 break; 265 266 default: 267 state = ERROR; 268 break; 269 } 270 break; 271 272 case IN_QUOTED_VALUE: 273 switch (c) { 274 case '"': 275 if (!escaped) { 276 // don't trim quoted strings; the spaces could be intentional. 277 result.put( 278 paramName.toString().trim().toLowerCase(), 279 paramValue.toString()); 280 state = VALUE_DONE; 281 } else { 282 escaped = false; 283 paramValue.append(c); 284 } 285 break; 286 287 case '\\': 288 if (escaped) { 289 paramValue.append('\\'); 290 } 291 escaped = !escaped; 292 break; 293 294 default: 295 if (escaped) { 296 paramValue.append('\\'); 297 } 298 escaped = false; 299 paramValue.append(c); 300 break; 301 } 302 break; 303 304 } 305 } 306 307 // done looping. check if anything is left over. 308 if (state == IN_VALUE) { 309 result.put( 310 paramName.toString().trim().toLowerCase(), 311 paramValue.toString().trim()); 312 } 313 } 314 315 return result; 316 } 317 318 319 public boolean isMimeType(String mimeType) { 320 return this.mimeType.equals(mimeType.toLowerCase()); 321 } 322 323 /** 324 * Return true if the BodyDescriptor belongs to a message 325 * 326 * @return 327 */ 328 public boolean isMessage() { 329 return mimeType.equals("message/rfc822"); 330 } 331 332 /** 333 * Retrun true if the BodyDescripotro belogns to a multipart 334 * 335 * @return 336 */ 337 public boolean isMultipart() { 338 return mimeType.startsWith("multipart/"); 339 } 340 341 /** 342 * Return the MimeType 343 * 344 * @return mimeType 345 */ 346 public String getMimeType() { 347 return mimeType; 348 } 349 350 /** 351 * Return the boundary 352 * 353 * @return boundary 354 */ 355 public String getBoundary() { 356 return boundary; 357 } 358 359 /** 360 * Return the charset 361 * 362 * @return charset 363 */ 364 public String getCharset() { 365 return charset; 366 } 367 368 /** 369 * Return all parameters for the BodyDescriptor 370 * 371 * @return parameters 372 */ 373 public Map getParameters() { 374 return parameters; 375 } 376 377 /** 378 * Return the TransferEncoding 379 * 380 * @return transferEncoding 381 */ 382 public String getTransferEncoding() { 383 return transferEncoding; 384 } 385 386 /** 387 * Return true if it's base64 encoded 388 * 389 * @return 390 * 391 */ 392 public boolean isBase64Encoded() { 393 return "base64".equals(transferEncoding); 394 } 395 396 /** 397 * Return true if it's quoted-printable 398 * @return 399 */ 400 public boolean isQuotedPrintableEncoded() { 401 return "quoted-printable".equals(transferEncoding); 402 } 403 404 public String toString() { 405 return mimeType; 406 } 407 } 408