/**************************************************************** * 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.message; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.apache.james.mime4j.field.ContentDispositionField; import org.apache.james.mime4j.field.ContentTransferEncodingField; import org.apache.james.mime4j.field.ContentTypeField; import org.apache.james.mime4j.field.FieldName; import org.apache.james.mime4j.field.Fields; import org.apache.james.mime4j.parser.Field; import org.apache.james.mime4j.util.MimeUtil; /** * MIME entity. An entity has a header and a body (see RFC 2045). */ public abstract class Entity implements Disposable { private Header header = null; private Body body = null; private Entity parent = null; /** * Creates a new <code>Entity</code>. Typically invoked implicitly by a * subclass constructor. */ protected Entity() { } /** * Creates a new <code>Entity</code> from the specified * <code>Entity</code>. The <code>Entity</code> instance is initialized * with copies of header and body of the specified <code>Entity</code>. * The parent entity of the new entity is <code>null</code>. * * @param other * entity to copy. * @throws UnsupportedOperationException * if <code>other</code> contains a {@link SingleBody} that * does not support the {@link SingleBody#copy() copy()} * operation. * @throws IllegalArgumentException * if <code>other</code> contains a <code>Body</code> that * is neither a {@link Message}, {@link Multipart} or * {@link SingleBody}. */ protected Entity(Entity other) { if (other.header != null) { header = new Header(other.header); } if (other.body != null) { Body bodyCopy = BodyCopier.copy(other.body); setBody(bodyCopy); } } /** * Gets the parent entity of this entity. * Returns <code>null</code> if this is the root entity. * * @return the parent or <code>null</code>. */ public Entity getParent() { return parent; } /** * Sets the parent entity of this entity. * * @param parent the parent entity or <code>null</code> if * this will be the root entity. */ public void setParent(Entity parent) { this.parent = parent; } /** * Gets the entity header. * * @return the header. */ public Header getHeader() { return header; } /** * Sets the entity header. * * @param header the header. */ public void setHeader(Header header) { this.header = header; } /** * Gets the body of this entity. * * @return the body, */ public Body getBody() { return body; } /** * Sets the body of this entity. * * @param body the body. * @throws IllegalStateException if the body has already been set. */ public void setBody(Body body) { if (this.body != null) throw new IllegalStateException("body already set"); this.body = body; body.setParent(this); } /** * Removes and returns the body of this entity. The removed body may be * attached to another entity. If it is no longer needed it should be * {@link Disposable#dispose() disposed} of. * * @return the removed body or <code>null</code> if no body was set. */ public Body removeBody() { if (body == null) return null; Body body = this.body; this.body = null; body.setParent(null); return body; } /** * Sets the specified message as body of this entity and the content type to * "message/rfc822". A <code>Header</code> is created if this * entity does not already have one. * * @param message * the message to set as body. */ public void setMessage(Message message) { setBody(message, "message/rfc822", null); } /** * Sets the specified multipart as body of this entity. Also sets the * content type accordingly and creates a message boundary string. A * <code>Header</code> is created if this entity does not already have * one. * * @param multipart * the multipart to set as body. */ public void setMultipart(Multipart multipart) { String mimeType = "multipart/" + multipart.getSubType(); Map<String, String> parameters = Collections.singletonMap("boundary", MimeUtil.createUniqueBoundary()); setBody(multipart, mimeType, parameters); } /** * Sets the specified multipart as body of this entity. Also sets the * content type accordingly and creates a message boundary string. A * <code>Header</code> is created if this entity does not already have * one. * * @param multipart * the multipart to set as body. * @param parameters * additional parameters for the Content-Type header field. */ public void setMultipart(Multipart multipart, Map<String, String> parameters) { String mimeType = "multipart/" + multipart.getSubType(); if (!parameters.containsKey("boundary")) { parameters = new HashMap<String, String>(parameters); parameters.put("boundary", MimeUtil.createUniqueBoundary()); } setBody(multipart, mimeType, parameters); } /** * Sets the specified <code>TextBody</code> as body of this entity and the * content type to "text/plain". A <code>Header</code> is * created if this entity does not already have one. * * @param textBody * the <code>TextBody</code> to set as body. * @see BodyFactory#textBody(String) */ public void setText(TextBody textBody) { setText(textBody, "plain"); } /** * Sets the specified <code>TextBody</code> as body of this entity. Also * sets the content type according to the specified sub-type. A * <code>Header</code> is created if this entity does not already have * one. * * @param textBody * the <code>TextBody</code> to set as body. * @param subtype * the text subtype (e.g. "plain", "html" or * "xml"). * @see BodyFactory#textBody(String) */ public void setText(TextBody textBody, String subtype) { String mimeType = "text/" + subtype; Map<String, String> parameters = null; String mimeCharset = textBody.getMimeCharset(); if (mimeCharset != null && !mimeCharset.equalsIgnoreCase("us-ascii")) { parameters = Collections.singletonMap("charset", mimeCharset); } setBody(textBody, mimeType, parameters); } /** * Sets the body of this entity and sets the content-type to the specified * value. A <code>Header</code> is created if this entity does not already * have one. * * @param body * the body. * @param mimeType * the MIME media type of the specified body * ("type/subtype"). */ public void setBody(Body body, String mimeType) { setBody(body, mimeType, null); } /** * Sets the body of this entity and sets the content-type to the specified * value. A <code>Header</code> is created if this entity does not already * have one. * * @param body * the body. * @param mimeType * the MIME media type of the specified body * ("type/subtype"). * @param parameters * additional parameters for the Content-Type header field. */ public void setBody(Body body, String mimeType, Map<String, String> parameters) { setBody(body); Header header = obtainHeader(); header.setField(Fields.contentType(mimeType, parameters)); } /** * Determines the MIME type of this <code>Entity</code>. The MIME type * is derived by looking at the parent's Content-Type field if no * Content-Type field is set for this <code>Entity</code>. * * @return the MIME type. */ public String getMimeType() { ContentTypeField child = (ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE); ContentTypeField parent = getParent() != null ? (ContentTypeField) getParent().getHeader(). getField(FieldName.CONTENT_TYPE) : null; return ContentTypeField.getMimeType(child, parent); } /** * Determines the MIME character set encoding of this <code>Entity</code>. * * @return the MIME character set encoding. */ public String getCharset() { return ContentTypeField.getCharset( (ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE)); } /** * Determines the transfer encoding of this <code>Entity</code>. * * @return the transfer encoding. */ public String getContentTransferEncoding() { ContentTransferEncodingField f = (ContentTransferEncodingField) getHeader().getField(FieldName.CONTENT_TRANSFER_ENCODING); return ContentTransferEncodingField.getEncoding(f); } /** * Sets the transfer encoding of this <code>Entity</code> to the specified * value. * * @param contentTransferEncoding * transfer encoding to use. */ public void setContentTransferEncoding(String contentTransferEncoding) { Header header = obtainHeader(); header.setField(Fields.contentTransferEncoding(contentTransferEncoding)); } /** * Return the disposition type of the content disposition of this * <code>Entity</code>. * * @return the disposition type or <code>null</code> if no disposition * type has been set. */ public String getDispositionType() { ContentDispositionField field = obtainField(FieldName.CONTENT_DISPOSITION); if (field == null) return null; return field.getDispositionType(); } /** * Sets the content disposition of this <code>Entity</code> to the * specified disposition type. No filename, size or date parameters * are included in the content disposition. * * @param dispositionType * disposition type value (usually <code>inline</code> or * <code>attachment</code>). */ public void setContentDisposition(String dispositionType) { Header header = obtainHeader(); header.setField(Fields.contentDisposition(dispositionType, null, -1, null, null, null)); } /** * Sets the content disposition of this <code>Entity</code> to the * specified disposition type and filename. No size or date parameters are * included in the content disposition. * * @param dispositionType * disposition type value (usually <code>inline</code> or * <code>attachment</code>). * @param filename * filename parameter value or <code>null</code> if the * parameter should not be included. */ public void setContentDisposition(String dispositionType, String filename) { Header header = obtainHeader(); header.setField(Fields.contentDisposition(dispositionType, filename, -1, null, null, null)); } /** * Sets the content disposition of this <code>Entity</code> to the * specified values. No date parameters are included in the content * disposition. * * @param dispositionType * disposition type value (usually <code>inline</code> or * <code>attachment</code>). * @param filename * filename parameter value or <code>null</code> if the * parameter should not be included. * @param size * size parameter value or <code>-1</code> if the parameter * should not be included. */ public void setContentDisposition(String dispositionType, String filename, long size) { Header header = obtainHeader(); header.setField(Fields.contentDisposition(dispositionType, filename, size, null, null, null)); } /** * Sets the content disposition of this <code>Entity</code> to the * specified values. * * @param dispositionType * disposition type value (usually <code>inline</code> or * <code>attachment</code>). * @param filename * filename parameter value or <code>null</code> if the * parameter should not be included. * @param size * size parameter value or <code>-1</code> if the parameter * should not be included. * @param creationDate * creation-date parameter value or <code>null</code> if the * parameter should not be included. * @param modificationDate * modification-date parameter value or <code>null</code> if * the parameter should not be included. * @param readDate * read-date parameter value or <code>null</code> if the * parameter should not be included. */ public void setContentDisposition(String dispositionType, String filename, long size, Date creationDate, Date modificationDate, Date readDate) { Header header = obtainHeader(); header.setField(Fields.contentDisposition(dispositionType, filename, size, creationDate, modificationDate, readDate)); } /** * Returns the filename parameter of the content disposition of this * <code>Entity</code>. * * @return the filename parameter of the content disposition or * <code>null</code> if the filename has not been set. */ public String getFilename() { ContentDispositionField field = obtainField(FieldName.CONTENT_DISPOSITION); if (field == null) return null; return field.getFilename(); } /** * Sets the filename parameter of the content disposition of this * <code>Entity</code> to the specified value. If this entity does not * have a content disposition header field a new one with disposition type * <code>attachment</code> is created. * * @param filename * filename parameter value or <code>null</code> if the * parameter should be removed. */ public void setFilename(String filename) { Header header = obtainHeader(); ContentDispositionField field = (ContentDispositionField) header .getField(FieldName.CONTENT_DISPOSITION); if (field == null) { if (filename != null) { header.setField(Fields.contentDisposition( ContentDispositionField.DISPOSITION_TYPE_ATTACHMENT, filename, -1, null, null, null)); } } else { String dispositionType = field.getDispositionType(); Map<String, String> parameters = new HashMap<String, String>(field .getParameters()); if (filename == null) { parameters.remove(ContentDispositionField.PARAM_FILENAME); } else { parameters .put(ContentDispositionField.PARAM_FILENAME, filename); } header.setField(Fields.contentDisposition(dispositionType, parameters)); } } /** * Determines if the MIME type of this <code>Entity</code> matches the * given one. MIME types are case-insensitive. * * @param type the MIME type to match against. * @return <code>true</code> on match, <code>false</code> otherwise. */ public boolean isMimeType(String type) { return getMimeType().equalsIgnoreCase(type); } /** * Determines if the MIME type of this <code>Entity</code> is * <code>multipart/*</code>. Since multipart-entities must have * a boundary parameter in the <code>Content-Type</code> field this * method returns <code>false</code> if no boundary exists. * * @return <code>true</code> on match, <code>false</code> otherwise. */ public boolean isMultipart() { ContentTypeField f = (ContentTypeField) getHeader().getField(FieldName.CONTENT_TYPE); return f != null && f.getBoundary() != null && getMimeType().startsWith(ContentTypeField.TYPE_MULTIPART_PREFIX); } /** * Disposes of the body of this entity. Note that the dispose call does not * get forwarded to the parent entity of this Entity. * * Subclasses that need to free resources should override this method and * invoke super.dispose(). * * @see org.apache.james.mime4j.message.Disposable#dispose() */ public void dispose() { if (body != null) { body.dispose(); } } /** * Obtains the header of this entity. Creates and sets a new header if this * entity's header is currently <code>null</code>. * * @return the header of this entity; never <code>null</code>. */ Header obtainHeader() { if (header == null) { header = new Header(); } return header; } /** * Obtains the header field with the specified name. * * @param <F> * concrete field type. * @param fieldName * name of the field to retrieve. * @return the header field or <code>null</code> if this entity has no * header or the header contains no such field. */ <F extends Field> F obtainField(String fieldName) { Header header = getHeader(); if (header == null) return null; @SuppressWarnings("unchecked") F field = (F) header.getField(fieldName); return field; } }