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 */ 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