| /**************************************************************** |
| * Licensed to the Apache Software Foundation (ASF) under one * |
| * or more contributor license agreements. See the NOTICE file * |
| * distributed with this work for additional information * |
| * regarding copyright ownership. The ASF licenses this file * |
| * to you under the Apache License, Version 2.0 (the * |
| * "License"); you may not use this file except in compliance * |
| * with the License. You may obtain a copy of the License at * |
| * * |
| * http://www.apache.org/licenses/LICENSE-2.0 * |
| * * |
| * Unless required by applicable law or agreed to in writing, * |
| * software distributed under the License is distributed on an * |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * |
| * KIND, either express or implied. See the License for the * |
| * specific language governing permissions and limitations * |
| * under the License. * |
| ****************************************************************/ |
| |
| package org.apache.james.mime4j.stream; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.james.mime4j.util.MimeUtil; |
| |
| /** |
| * Encapsulates the values of the MIME-specific header fields |
| * (which starts with <code>Content-</code>). |
| */ |
| public class DefaultBodyDescriptor implements MutableBodyDescriptor { |
| private static final String US_ASCII = "us-ascii"; |
| |
| private static final String SUB_TYPE_EMAIL = "rfc822"; |
| |
| private static final String MEDIA_TYPE_TEXT = "text"; |
| |
| private static final String MEDIA_TYPE_MESSAGE = "message"; |
| |
| private static final String EMAIL_MESSAGE_MIME_TYPE = MEDIA_TYPE_MESSAGE + "/" + SUB_TYPE_EMAIL; |
| |
| private static final String DEFAULT_SUB_TYPE = "plain"; |
| |
| private static final String DEFAULT_MEDIA_TYPE = MEDIA_TYPE_TEXT; |
| |
| private static final String DEFAULT_MIME_TYPE = DEFAULT_MEDIA_TYPE + "/" + DEFAULT_SUB_TYPE; |
| |
| private static Log log = LogFactory.getLog(DefaultBodyDescriptor.class); |
| |
| private String mediaType = DEFAULT_MEDIA_TYPE; |
| private String subType = DEFAULT_SUB_TYPE; |
| private String mimeType = DEFAULT_MIME_TYPE; |
| private String boundary = null; |
| private String charset = US_ASCII; |
| private String transferEncoding = "7bit"; |
| private Map<String, String> parameters = new HashMap<String, String>(); |
| private boolean contentTypeSet; |
| private boolean contentTransferEncSet; |
| private long contentLength = -1; |
| |
| /** |
| * Creates a new root <code>BodyDescriptor</code> instance. |
| */ |
| public DefaultBodyDescriptor() { |
| this(null); |
| } |
| |
| /** |
| * Creates a new <code>BodyDescriptor</code> instance. |
| * |
| * @param parent the descriptor of the parent or <code>null</code> if this |
| * is the root descriptor. |
| */ |
| public DefaultBodyDescriptor(BodyDescriptor parent) { |
| if (parent != null && MimeUtil.isSameMimeType("multipart/digest", parent.getMimeType())) { |
| mimeType = EMAIL_MESSAGE_MIME_TYPE; |
| subType = SUB_TYPE_EMAIL; |
| mediaType = MEDIA_TYPE_MESSAGE; |
| } else { |
| mimeType = DEFAULT_MIME_TYPE; |
| subType = DEFAULT_SUB_TYPE; |
| mediaType = DEFAULT_MEDIA_TYPE; |
| } |
| } |
| |
| public MutableBodyDescriptor newChild() { |
| return new DefaultBodyDescriptor(this); |
| } |
| |
| /** |
| * Should be called for each <code>Content-</code> header field of |
| * a MIME message or part. |
| * |
| * @param field the MIME field. |
| */ |
| public void addField(RawField field) { |
| String name = field.getName(); |
| String value = field.getBody(); |
| |
| name = name.trim().toLowerCase(); |
| |
| if (name.equals("content-transfer-encoding") && !contentTransferEncSet) { |
| contentTransferEncSet = true; |
| |
| value = value.trim().toLowerCase(); |
| if (value.length() > 0) { |
| transferEncoding = value; |
| } |
| |
| } else if (name.equals("content-length") && contentLength == -1) { |
| try { |
| contentLength = Long.parseLong(value.trim()); |
| } catch (NumberFormatException e) { |
| log.error("Invalid content-length: " + value); |
| } |
| } else if (name.equals("content-type") && !contentTypeSet) { |
| parseContentType(value); |
| } |
| } |
| |
| private void parseContentType(String value) { |
| contentTypeSet = true; |
| |
| Map<String, String> params = DefaultBodyDescriptor.getHeaderParams(value); |
| |
| String main = params.get(""); |
| String type = null; |
| String subtype = null; |
| if (main != null) { |
| main = main.toLowerCase().trim(); |
| int index = main.indexOf('/'); |
| boolean valid = false; |
| if (index != -1) { |
| type = main.substring(0, index).trim(); |
| subtype = main.substring(index + 1).trim(); |
| if (type.length() > 0 && subtype.length() > 0) { |
| main = type + "/" + subtype; |
| valid = true; |
| } |
| } |
| |
| if (!valid) { |
| main = null; |
| type = null; |
| subtype = null; |
| } |
| } |
| String b = params.get("boundary"); |
| |
| if (main != null |
| && ((main.startsWith("multipart/") && b != null) |
| || !main.startsWith("multipart/"))) { |
| mimeType = main; |
| this.subType = subtype; |
| this.mediaType = type; |
| } |
| |
| if (MimeUtil.isMultipart(mimeType)) { |
| boundary = b; |
| } |
| |
| String c = params.get("charset"); |
| charset = null; |
| if (c != null) { |
| c = c.trim(); |
| if (c.length() > 0) { |
| charset = c.toLowerCase(); |
| } |
| } |
| if (charset == null && MEDIA_TYPE_TEXT.equals(mediaType)) { |
| charset = US_ASCII; |
| } |
| |
| /* |
| * Add all other parameters to parameters. |
| */ |
| parameters.putAll(params); |
| parameters.remove(""); |
| parameters.remove("boundary"); |
| parameters.remove("charset"); |
| } |
| |
| /** |
| * Return the MimeType |
| * |
| * @return mimeType |
| */ |
| public String getMimeType() { |
| return mimeType; |
| } |
| |
| /** |
| * Return the boundary |
| * |
| * @return boundary |
| */ |
| public String getBoundary() { |
| return boundary; |
| } |
| |
| /** |
| * Return the charset |
| * |
| * @return charset |
| */ |
| public String getCharset() { |
| return charset; |
| } |
| |
| /** |
| * Return all parameters for the BodyDescriptor |
| * |
| * @return parameters |
| */ |
| public Map<String, String> getContentTypeParameters() { |
| return parameters; |
| } |
| |
| /** |
| * Return the TransferEncoding |
| * |
| * @return transferEncoding |
| */ |
| public String getTransferEncoding() { |
| return transferEncoding; |
| } |
| |
| @Override |
| public String toString() { |
| return mimeType; |
| } |
| |
| public long getContentLength() { |
| return contentLength; |
| } |
| |
| public String getMediaType() { |
| return mediaType; |
| } |
| |
| public String getSubType() { |
| return subType; |
| } |
| |
| /** |
| * <p>Parses a complex field value into a map of key/value pairs. You may |
| * use this, for example, to parse a definition like |
| * <pre> |
| * text/plain; charset=UTF-8; boundary=foobar |
| * </pre> |
| * The above example would return a map with the keys "", "charset", |
| * and "boundary", and the values "text/plain", "UTF-8", and "foobar". |
| * </p><p> |
| * Header value will be unfolded and excess white space trimmed. |
| * </p> |
| * @param pValue The field value to parse. |
| * @return The result map; use the key "" to retrieve the first value. |
| */ |
| public static Map<String, String> getHeaderParams(String pValue) { |
| pValue = pValue.trim(); |
| |
| Map<String, String> result = new HashMap<String, String>(); |
| |
| // split main value and parameters |
| String main; |
| String rest; |
| if (pValue.indexOf(";") == -1) { |
| main = pValue; |
| rest = null; |
| } else { |
| main = pValue.substring(0, pValue.indexOf(";")); |
| rest = pValue.substring(main.length() + 1); |
| } |
| |
| result.put("", main); |
| if (rest != null) { |
| char[] chars = rest.toCharArray(); |
| StringBuilder paramName = new StringBuilder(64); |
| StringBuilder paramValue = new StringBuilder(64); |
| |
| final byte READY_FOR_NAME = 0; |
| final byte IN_NAME = 1; |
| final byte READY_FOR_VALUE = 2; |
| final byte IN_VALUE = 3; |
| final byte IN_QUOTED_VALUE = 4; |
| final byte VALUE_DONE = 5; |
| final byte ERROR = 99; |
| |
| byte state = READY_FOR_NAME; |
| boolean escaped = false; |
| for (char c : chars) { |
| switch (state) { |
| case ERROR: |
| if (c == ';') |
| state = READY_FOR_NAME; |
| break; |
| |
| case READY_FOR_NAME: |
| if (c == '=') { |
| log.error("Expected header param name, got '='"); |
| state = ERROR; |
| break; |
| } |
| |
| paramName.setLength(0); |
| paramValue.setLength(0); |
| |
| state = IN_NAME; |
| // fall-through |
| |
| case IN_NAME: |
| if (c == '=') { |
| if (paramName.length() == 0) |
| state = ERROR; |
| else |
| state = READY_FOR_VALUE; |
| break; |
| } |
| |
| // not '='... just add to name |
| paramName.append(c); |
| break; |
| |
| case READY_FOR_VALUE: |
| boolean fallThrough = false; |
| switch (c) { |
| case ' ': |
| case '\t': |
| break; // ignore spaces, especially before '"' |
| |
| case '"': |
| state = IN_QUOTED_VALUE; |
| break; |
| |
| default: |
| state = IN_VALUE; |
| fallThrough = true; |
| break; |
| } |
| if (!fallThrough) |
| break; |
| |
| // fall-through |
| |
| case IN_VALUE: |
| fallThrough = false; |
| switch (c) { |
| case ';': |
| case ' ': |
| case '\t': |
| result.put( |
| paramName.toString().trim().toLowerCase(), |
| paramValue.toString().trim()); |
| state = VALUE_DONE; |
| fallThrough = true; |
| break; |
| default: |
| paramValue.append(c); |
| break; |
| } |
| if (!fallThrough) |
| break; |
| |
| case VALUE_DONE: |
| switch (c) { |
| case ';': |
| state = READY_FOR_NAME; |
| break; |
| |
| case ' ': |
| case '\t': |
| break; |
| |
| default: |
| state = ERROR; |
| break; |
| } |
| break; |
| |
| case IN_QUOTED_VALUE: |
| switch (c) { |
| case '"': |
| if (!escaped) { |
| // don't trim quoted strings; the spaces could be intentional. |
| result.put( |
| paramName.toString().trim().toLowerCase(), |
| paramValue.toString()); |
| state = VALUE_DONE; |
| } else { |
| escaped = false; |
| paramValue.append(c); |
| } |
| break; |
| |
| case '\\': |
| if (escaped) { |
| paramValue.append('\\'); |
| } |
| escaped = !escaped; |
| break; |
| |
| default: |
| if (escaped) { |
| paramValue.append('\\'); |
| } |
| escaped = false; |
| paramValue.append(c); |
| break; |
| } |
| break; |
| |
| } |
| } |
| |
| // done looping. check if anything is left over. |
| if (state == IN_VALUE) { |
| result.put( |
| paramName.toString().trim().toLowerCase(), |
| paramValue.toString().trim()); |
| } |
| } |
| |
| return result; |
| } |
| } |