blob: 222139787d17578526f10a76b5a14a8226062d23 [file] [log] [blame]
/****************************************************************
* 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;
}
}