Java tutorial
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2018 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://oss.oracle.com/licenses/CDDL+GPL-1.1 * or LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package javax.mail.internet; import javax.mail.*; import javax.activation.*; import java.io.*; import java.util.*; import com.sun.mail.util.PropUtil; import com.sun.mail.util.ASCIIUtility; import com.sun.mail.util.MimeUtil; import com.sun.mail.util.MessageRemovedIOException; import com.sun.mail.util.FolderClosedIOException; import com.sun.mail.util.LineOutputStream; /** * This class represents a MIME body part. It implements the * <code>BodyPart</code> abstract class and the <code>MimePart</code> * interface. MimeBodyParts are contained in <code>MimeMultipart</code> * objects. <p> * * MimeBodyPart uses the <code>InternetHeaders</code> class to parse * and store the headers of that body part. * * <hr><strong>A note on RFC 822 and MIME headers</strong><p> * * RFC 822 header fields <strong>must</strong> contain only * US-ASCII characters. MIME allows non ASCII characters to be present * in certain portions of certain headers, by encoding those characters. * RFC 2047 specifies the rules for doing this. The MimeUtility * class provided in this package can be used to to achieve this. * Callers of the <code>setHeader</code>, <code>addHeader</code>, and * <code>addHeaderLine</code> methods are responsible for enforcing * the MIME requirements for the specified headers. In addition, these * header fields must be folded (wrapped) before being sent if they * exceed the line length limitation for the transport (1000 bytes for * SMTP). Received headers may have been folded. The application is * responsible for folding and unfolding headers as appropriate. <p> * * @author John Mani * @author Bill Shannon * @author Kanwar Oberoi * @see javax.mail.Part * @see javax.mail.internet.MimePart * @see javax.mail.internet.MimeUtility */ public class MimeBodyPart extends BodyPart implements MimePart { // Paranoia: // allow this last minute change to be disabled if it causes problems private static final boolean setDefaultTextCharset = PropUtil .getBooleanSystemProperty("mail.mime.setdefaulttextcharset", true); private static final boolean setContentTypeFileName = PropUtil .getBooleanSystemProperty("mail.mime.setcontenttypefilename", true); private static final boolean encodeFileName = PropUtil.getBooleanSystemProperty("mail.mime.encodefilename", false); private static final boolean decodeFileName = PropUtil.getBooleanSystemProperty("mail.mime.decodefilename", false); private static final boolean ignoreMultipartEncoding = PropUtil .getBooleanSystemProperty("mail.mime.ignoremultipartencoding", true); private static final boolean allowutf8 = PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", true); // Paranoia: // allow this last minute change to be disabled if it causes problems static final boolean cacheMultipart = // accessed by MimeMessage PropUtil.getBooleanSystemProperty("mail.mime.cachemultipart", true); /** * The DataHandler object representing this Part's content. */ protected DataHandler dh; /** * Byte array that holds the bytes of the content of this Part. */ protected byte[] content; /** * If the data for this body part was supplied by an * InputStream that implements the SharedInputStream interface, * <code>contentStream</code> is another such stream representing * the content of this body part. In this case, <code>content</code> * will be null. * * @since JavaMail 1.2 */ protected InputStream contentStream; /** * The InternetHeaders object that stores all the headers * of this body part. */ protected InternetHeaders headers; /** * If our content is a Multipart of Message object, we save it * the first time it's created by parsing a stream so that changes * to the contained objects will not be lost. * * If this field is not null, it's return by the {@link #getContent} * method. The {@link #getContent} method sets this field if it * would return a Multipart or MimeMessage object. This field is * is cleared by the {@link #setDataHandler} method. * * @since JavaMail 1.5 */ protected Object cachedContent; /** * An empty MimeBodyPart object is created. * This body part maybe filled in by a client constructing a multipart * message. */ public MimeBodyPart() { super(); headers = new InternetHeaders(); } /** * Constructs a MimeBodyPart by reading and parsing the data from * the specified input stream. The parser consumes data till the end * of the given input stream. The input stream must start at the * beginning of a valid MIME body part and must terminate at the end * of that body part. <p> * * Note that the "boundary" string that delimits body parts must * <strong>not</strong> be included in the input stream. The intention * is that the MimeMultipart parser will extract each body part's bytes * from a multipart stream and feed them into this constructor, without * the delimiter strings. * * @param is the body part Input Stream * @exception MessagingException for failures */ public MimeBodyPart(InputStream is) throws MessagingException { if (!(is instanceof ByteArrayInputStream) && !(is instanceof BufferedInputStream) && !(is instanceof SharedInputStream)) is = new BufferedInputStream(is); headers = new InternetHeaders(is); if (is instanceof SharedInputStream) { SharedInputStream sis = (SharedInputStream) is; contentStream = sis.newStream(sis.getPosition(), -1); } else { try { content = ASCIIUtility.getBytes(is); } catch (IOException ioex) { throw new MessagingException("Error reading input stream", ioex); } } } /** * Constructs a MimeBodyPart using the given header and * content bytes. <p> * * Used by providers. * * @param headers The header of this part * @param content bytes representing the body of this part. * @exception MessagingException for failures */ public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException { super(); this.headers = headers; this.content = content; } /** * Return the size of the content of this body part in bytes. * Return -1 if the size cannot be determined. <p> * * Note that this number may not be an exact measure of the * content size and may or may not account for any transfer * encoding of the content. <p> * * This implementation returns the size of the <code>content</code> * array (if not null), or, if <code>contentStream</code> is not * null, and the <code>available</code> method returns a positive * number, it returns that number as the size. Otherwise, it returns * -1. * * @return size in bytes, or -1 if not known */ @Override public int getSize() throws MessagingException { if (content != null) return content.length; if (contentStream != null) { try { int size = contentStream.available(); // only believe the size if it's greate than zero, since zero // is the default returned by the InputStream class itself if (size > 0) return size; } catch (IOException ex) { // ignore it } } return -1; } /** * Return the number of lines for the content of this Part. * Return -1 if this number cannot be determined. <p> * * Note that this number may not be an exact measure of the * content length and may or may not account for any transfer * encoding of the content. <p> * * This implementation returns -1. * * @return number of lines, or -1 if not known */ @Override public int getLineCount() throws MessagingException { return -1; } /** * Returns the value of the RFC 822 "Content-Type" header field. * This represents the content type of the content of this * body part. This value must not be null. If this field is * unavailable, "text/plain" should be returned. <p> * * This implementation uses <code>getHeader(name)</code> * to obtain the requisite header field. * * @return Content-Type of this body part */ @Override public String getContentType() throws MessagingException { String s = getHeader("Content-Type", null); s = MimeUtil.cleanContentType(this, s); if (s == null) s = "text/plain"; return s; } /** * Is this Part of the specified MIME type? This method * compares <strong>only the <code>primaryType</code> and * <code>subType</code></strong>. * The parameters of the content types are ignored. <p> * * For example, this method will return <code>true</code> when * comparing a Part of content type <strong>"text/plain"</strong> * with <strong>"text/plain; charset=foobar"</strong>. <p> * * If the <code>subType</code> of <code>mimeType</code> is the * special character '*', then the subtype is ignored during the * comparison. * * @exception MessagingException for failures */ @Override public boolean isMimeType(String mimeType) throws MessagingException { return isMimeType(this, mimeType); } /** * Returns the disposition from the "Content-Disposition" header field. * This represents the disposition of this part. The disposition * describes how the part should be presented to the user. <p> * * If the Content-Disposition field is unavailable, * null is returned. <p> * * This implementation uses <code>getHeader(name)</code> * to obtain the requisite header field. * * @exception MessagingException for failures * @see #headers */ @Override public String getDisposition() throws MessagingException { return getDisposition(this); } /** * Set the disposition in the "Content-Disposition" header field * of this body part. If the disposition is null, any existing * "Content-Disposition" header field is removed. * * @exception IllegalWriteException if the underlying * implementation does not support modification * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. * @exception MessagingException for other failures */ @Override public void setDisposition(String disposition) throws MessagingException { setDisposition(this, disposition); } /** * Returns the content transfer encoding from the * "Content-Transfer-Encoding" header * field. Returns <code>null</code> if the header is unavailable * or its value is absent. <p> * * This implementation uses <code>getHeader(name)</code> * to obtain the requisite header field. * * @see #headers */ @Override public String getEncoding() throws MessagingException { return getEncoding(this); } /** * Returns the value of the "Content-ID" header field. Returns * <code>null</code> if the field is unavailable or its value is * absent. <p> * * This implementation uses <code>getHeader(name)</code> * to obtain the requisite header field. */ @Override public String getContentID() throws MessagingException { return getHeader("Content-Id", null); } /** * Set the "Content-ID" header field of this body part. * If the <code>cid</code> parameter is null, any existing * "Content-ID" is removed. * * @param cid the Content-ID * @exception IllegalWriteException if the underlying * implementation does not support modification * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. * @exception MessagingException for other failures * @since JavaMail 1.3 */ public void setContentID(String cid) throws MessagingException { if (cid == null) removeHeader("Content-ID"); else setHeader("Content-ID", cid); } /** * Return the value of the "Content-MD5" header field. Returns * <code>null</code> if this field is unavailable or its value * is absent. <p> * * This implementation uses <code>getHeader(name)</code> * to obtain the requisite header field. */ @Override public String getContentMD5() throws MessagingException { return getHeader("Content-MD5", null); } /** * Set the "Content-MD5" header field of this body part. * * @exception IllegalWriteException if the underlying * implementation does not support modification * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. */ @Override public void setContentMD5(String md5) throws MessagingException { setHeader("Content-MD5", md5); } /** * Get the languages specified in the Content-Language header * of this MimePart. The Content-Language header is defined by * RFC 1766. Returns <code>null</code> if this header is not * available or its value is absent. <p> * * This implementation uses <code>getHeader(name)</code> * to obtain the requisite header field. */ @Override public String[] getContentLanguage() throws MessagingException { return getContentLanguage(this); } /** * Set the Content-Language header of this MimePart. The * Content-Language header is defined by RFC 1766. * * @param languages array of language tags */ @Override public void setContentLanguage(String[] languages) throws MessagingException { setContentLanguage(this, languages); } /** * Returns the "Content-Description" header field of this body part. * This typically associates some descriptive information with * this part. Returns null if this field is unavailable or its * value is absent. <p> * * If the Content-Description field is encoded as per RFC 2047, * it is decoded and converted into Unicode. If the decoding or * conversion fails, the raw data is returned as is. <p> * * This implementation uses <code>getHeader(name)</code> * to obtain the requisite header field. * * @return content description */ @Override public String getDescription() throws MessagingException { return getDescription(this); } /** * Set the "Content-Description" header field for this body part. * If the description parameter is <code>null</code>, then any * existing "Content-Description" fields are removed. <p> * * If the description contains non US-ASCII characters, it will * be encoded using the platform's default charset. If the * description contains only US-ASCII characters, no encoding * is done and it is used as is. <p> * * Note that if the charset encoding process fails, a * MessagingException is thrown, and an UnsupportedEncodingException * is included in the chain of nested exceptions within the * MessagingException. * * @param description content description * @exception IllegalWriteException if the underlying * implementation does not support modification * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. * @exception MessagingException otherwise; an * UnsupportedEncodingException may be included * in the exception chain if the charset * conversion fails. */ @Override public void setDescription(String description) throws MessagingException { setDescription(description, null); } /** * Set the "Content-Description" header field for this body part. * If the description parameter is <code>null</code>, then any * existing "Content-Description" fields are removed. <p> * * If the description contains non US-ASCII characters, it will * be encoded using the specified charset. If the description * contains only US-ASCII characters, no encoding is done and * it is used as is. <p> * * Note that if the charset encoding process fails, a * MessagingException is thrown, and an UnsupportedEncodingException * is included in the chain of nested exceptions within the * MessagingException. * * @param description Description * @param charset Charset for encoding * @exception IllegalWriteException if the underlying * implementation does not support modification * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. * @exception MessagingException otherwise; an * UnsupportedEncodingException may be included * in the exception chain if the charset * conversion fails. */ public void setDescription(String description, String charset) throws MessagingException { setDescription(this, description, charset); } /** * Get the filename associated with this body part. <p> * * Returns the value of the "filename" parameter from the * "Content-Disposition" header field of this body part. If its * not available, returns the value of the "name" parameter from * the "Content-Type" header field of this body part. * Returns <code>null</code> if both are absent. <p> * * If the <code>mail.mime.decodefilename</code> System property * is set to true, the {@link MimeUtility#decodeText * MimeUtility.decodeText} method will be used to decode the * filename. While such encoding is not supported by the MIME * spec, many mailers use this technique to support non-ASCII * characters in filenames. The default value of this property * is false. * * @return filename */ @Override public String getFileName() throws MessagingException { return getFileName(this); } /** * Set the filename associated with this body part, if possible. <p> * * Sets the "filename" parameter of the "Content-Disposition" * header field of this body part. For compatibility with older * mailers, the "name" parameter of the "Content-Type" header is * also set. <p> * * If the <code>mail.mime.encodefilename</code> System property * is set to true, the {@link MimeUtility#encodeText * MimeUtility.encodeText} method will be used to encode the * filename. While such encoding is not supported by the MIME * spec, many mailers use this technique to support non-ASCII * characters in filenames. The default value of this property * is false. * * @param filename the file name * @exception IllegalWriteException if the underlying * implementation does not support modification * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. * @exception MessagingException for other failures */ @Override public void setFileName(String filename) throws MessagingException { setFileName(this, filename); } /** * Return a decoded input stream for this body part's "content". <p> * * This implementation obtains the input stream from the DataHandler. * That is, it invokes getDataHandler().getInputStream(); * * @return an InputStream * @exception IOException this is typically thrown by the * DataHandler. Refer to the documentation for * javax.activation.DataHandler for more details. * @exception MessagingException for other failures * * @see #getContentStream * @see javax.activation.DataHandler#getInputStream */ @Override public InputStream getInputStream() throws IOException, MessagingException { return getDataHandler().getInputStream(); } /** * Produce the raw bytes of the content. This method is used * when creating a DataHandler object for the content. Subclasses * that can provide a separate input stream for just the Part * content might want to override this method. <p> * * @return an InputStream containing the raw bytes * @exception MessagingException for failures * @see #content * @see MimeMessage#getContentStream */ protected InputStream getContentStream() throws MessagingException { if (contentStream != null) return ((SharedInputStream) contentStream).newStream(0, -1); if (content != null) return new ByteArrayInputStream(content); throw new MessagingException("No MimeBodyPart content"); } /** * Return an InputStream to the raw data with any Content-Transfer-Encoding * intact. This method is useful if the "Content-Transfer-Encoding" * header is incorrect or corrupt, which would prevent the * <code>getInputStream</code> method or <code>getContent</code> method * from returning the correct data. In such a case the application may * use this method and attempt to decode the raw data itself. <p> * * This implementation simply calls the <code>getContentStream</code> * method. * * @return an InputStream containing the raw bytes * @exception MessagingException for failures * @see #getInputStream * @see #getContentStream * @since JavaMail 1.2 */ public InputStream getRawInputStream() throws MessagingException { return getContentStream(); } /** * Return a DataHandler for this body part's content. <p> * * The implementation provided here works just like the * the implementation in MimeMessage. * @see MimeMessage#getDataHandler */ @Override public DataHandler getDataHandler() throws MessagingException { if (dh == null) dh = new MimePartDataHandler(this); return dh; } /** * Return the content as a Java object. The type of the object * returned is of course dependent on the content itself. For * example, the native format of a text/plain content is usually * a String object. The native format for a "multipart" * content is always a Multipart subclass. For content types that are * unknown to the DataHandler system, an input stream is returned * as the content. <p> * * This implementation obtains the content from the DataHandler. * That is, it invokes getDataHandler().getContent(); * If the content is a Multipart or Message object and was created by * parsing a stream, the object is cached and returned in subsequent * calls so that modifications to the content will not be lost. * * @return Object * @exception IOException this is typically thrown by the * DataHandler. Refer to the documentation for * javax.activation.DataHandler for more details. * @exception MessagingException for other failures */ @Override public Object getContent() throws IOException, MessagingException { if (cachedContent != null) return cachedContent; Object c; try { c = getDataHandler().getContent(); } catch (FolderClosedIOException fex) { throw new FolderClosedException(fex.getFolder(), fex.getMessage()); } catch (MessageRemovedIOException mex) { throw new MessageRemovedException(mex.getMessage()); } if (cacheMultipart && (c instanceof Multipart || c instanceof Message) && (content != null || contentStream != null)) { cachedContent = c; /* * We may abandon the input stream so make sure * the MimeMultipart has consumed the stream. */ if (c instanceof MimeMultipart) ((MimeMultipart) c).parse(); } return c; } /** * This method provides the mechanism to set this body part's content. * The given DataHandler object should wrap the actual content. * * @param dh The DataHandler for the content * @exception IllegalWriteException if the underlying * implementation does not support modification * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. */ @Override public void setDataHandler(DataHandler dh) throws MessagingException { this.dh = dh; cachedContent = null; MimeBodyPart.invalidateContentHeaders(this); } /** * A convenience method for setting this body part's content. <p> * * The content is wrapped in a DataHandler object. Note that a * DataContentHandler class for the specified type should be * available to the JavaMail implementation for this to work right. * That is, to do <code>setContent(foobar, "application/x-foobar")</code>, * a DataContentHandler for "application/x-foobar" should be installed. * Refer to the Java Activation Framework for more information. * * @param o the content object * @param type Mime type of the object * @exception IllegalWriteException if the underlying * implementation does not support modification of * existing values * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. */ @Override public void setContent(Object o, String type) throws MessagingException { if (o instanceof Multipart) { setContent((Multipart) o); } else { setDataHandler(new DataHandler(o, type)); } } /** * Convenience method that sets the given String as this * part's content, with a MIME type of "text/plain". If the * string contains non US-ASCII characters, it will be encoded * using the platform's default charset. The charset is also * used to set the "charset" parameter. <p> * * Note that there may be a performance penalty if * <code>text</code> is large, since this method may have * to scan all the characters to determine what charset to * use. <p> * * If the charset is already known, use the * <code>setText</code> method that takes the charset parameter. * * @param text the text content to set * @exception MessagingException if an error occurs * @see #setText(String text, String charset) */ @Override public void setText(String text) throws MessagingException { setText(text, null); } /** * Convenience method that sets the given String as this part's * content, with a MIME type of "text/plain" and the specified * charset. The given Unicode string will be charset-encoded * using the specified charset. The charset is also used to set * the "charset" parameter. * * @param text the text content to set * @param charset the charset to use for the text * @exception MessagingException if an error occurs */ @Override public void setText(String text, String charset) throws MessagingException { setText(this, text, charset, "plain"); } /** * Convenience method that sets the given String as this part's * content, with a primary MIME type of "text" and the specified * MIME subtype. The given Unicode string will be charset-encoded * using the specified charset. The charset is also used to set * the "charset" parameter. * * @param text the text content to set * @param charset the charset to use for the text * @param subtype the MIME subtype to use (e.g., "html") * @exception MessagingException if an error occurs * @since JavaMail 1.4 */ @Override public void setText(String text, String charset, String subtype) throws MessagingException { setText(this, text, charset, subtype); } /** * This method sets the body part's content to a Multipart object. * * @param mp The multipart object that is the Message's content * @exception IllegalWriteException if the underlying * implementation does not support modification of * existing values. * @exception IllegalStateException if this body part is * obtained from a READ_ONLY folder. */ @Override public void setContent(Multipart mp) throws MessagingException { setDataHandler(new DataHandler(mp, mp.getContentType())); mp.setParent(this); } /** * Use the specified file to provide the data for this part. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The encoding will be chosen appropriately for the * file data. The disposition of this part is set to * {@link Part#ATTACHMENT Part.ATTACHMENT}. * * @param file the File object to attach * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.4 */ public void attachFile(File file) throws IOException, MessagingException { FileDataSource fds = new FileDataSource(file); this.setDataHandler(new DataHandler(fds)); this.setFileName(fds.getName()); this.setDisposition(ATTACHMENT); } /** * Use the specified file to provide the data for this part. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The encoding will be chosen appropriately for the * file data. * * @param file the name of the file to attach * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.4 */ public void attachFile(String file) throws IOException, MessagingException { File f = new File(file); attachFile(f); } /** * Use the specified file with the specified Content-Type and * Content-Transfer-Encoding to provide the data for this part. * If contentType or encoding are null, appropriate values will * be chosen. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The disposition of this part is set to * {@link Part#ATTACHMENT Part.ATTACHMENT}. * * @param file the File object to attach * @param contentType the Content-Type, or null * @param encoding the Content-Transfer-Encoding, or null * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.5 */ public void attachFile(File file, String contentType, String encoding) throws IOException, MessagingException { DataSource fds = new EncodedFileDataSource(file, contentType, encoding); this.setDataHandler(new DataHandler(fds)); this.setFileName(fds.getName()); this.setDisposition(ATTACHMENT); } /** * Use the specified file with the specified Content-Type and * Content-Transfer-Encoding to provide the data for this part. * If contentType or encoding are null, appropriate values will * be chosen. * The simple file name is used as the file name for this * part and the data in the file is used as the data for this * part. The disposition of this part is set to * {@link Part#ATTACHMENT Part.ATTACHMENT}. * * @param file the name of the file * @param contentType the Content-Type, or null * @param encoding the Content-Transfer-Encoding, or null * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.5 */ public void attachFile(String file, String contentType, String encoding) throws IOException, MessagingException { attachFile(new File(file), contentType, encoding); } /** * A FileDataSource class that allows us to specify the * Content-Type and Content-Transfer-Encoding. */ private static class EncodedFileDataSource extends FileDataSource implements EncodingAware { private String contentType; private String encoding; public EncodedFileDataSource(File file, String contentType, String encoding) { super(file); this.contentType = contentType; this.encoding = encoding; } // overrides DataSource.getContentType() @Override public String getContentType() { return contentType != null ? contentType : super.getContentType(); } // implements EncodingAware.getEncoding() @Override public String getEncoding() { return encoding; } } /** * Save the contents of this part in the specified file. The content * is decoded and saved, without any of the MIME headers. * * @param file the File object to write to * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.4 */ public void saveFile(File file) throws IOException, MessagingException { OutputStream out = null; InputStream in = null; try { out = new BufferedOutputStream(new FileOutputStream(file)); in = this.getInputStream(); byte[] buf = new byte[8192]; int len; while ((len = in.read(buf)) > 0) out.write(buf, 0, len); } finally { // close streams, but don't mask original exception, if any try { if (in != null) in.close(); } catch (IOException ex) { } try { if (out != null) out.close(); } catch (IOException ex) { } } } /** * Save the contents of this part in the specified file. The content * is decoded and saved, without any of the MIME headers. * * @param file the name of the file to write to * @exception IOException errors related to accessing the file * @exception MessagingException message related errors * @since JavaMail 1.4 */ public void saveFile(String file) throws IOException, MessagingException { File f = new File(file); saveFile(f); } /** * Output the body part as an RFC 822 format stream. * * @exception IOException if an error occurs writing to the * stream or if an error is generated * by the javax.activation layer. * @exception MessagingException for other failures * @see javax.activation.DataHandler#writeTo */ @Override public void writeTo(OutputStream os) throws IOException, MessagingException { writeTo(this, os, null); } /** * Get all the headers for this header_name. Note that certain * headers may be encoded as per RFC 2047 if they contain * non US-ASCII characters and these should be decoded. * * @param name name of header * @return array of headers * @see javax.mail.internet.MimeUtility */ @Override public String[] getHeader(String name) throws MessagingException { return headers.getHeader(name); } /** * Get all the headers for this header name, returned as a single * String, with headers separated by the delimiter. If the * delimiter is <code>null</code>, only the first header is * returned. * * @param name the name of this header * @param delimiter delimiter between fields in returned string * @return the value fields for all headers with * this name * @exception MessagingException for failures */ @Override public String getHeader(String name, String delimiter) throws MessagingException { return headers.getHeader(name, delimiter); } /** * Set the value for this header_name. Replaces all existing * header values with this new value. Note that RFC 822 headers * must contain only US-ASCII characters, so a header that * contains non US-ASCII characters must be encoded as per the * rules of RFC 2047. * * @param name header name * @param value header value * @see javax.mail.internet.MimeUtility */ @Override public void setHeader(String name, String value) throws MessagingException { headers.setHeader(name, value); } /** * Add this value to the existing values for this header_name. * Note that RFC 822 headers must contain only US-ASCII * characters, so a header that contains non US-ASCII characters * must be encoded as per the rules of RFC 2047. * * @param name header name * @param value header value * @see javax.mail.internet.MimeUtility */ @Override public void addHeader(String name, String value) throws MessagingException { headers.addHeader(name, value); } /** * Remove all headers with this name. */ @Override public void removeHeader(String name) throws MessagingException { headers.removeHeader(name); } /** * Return all the headers from this Message as an Enumeration of * Header objects. */ @Override public Enumeration<Header> getAllHeaders() throws MessagingException { return headers.getAllHeaders(); } /** * Return matching headers from this Message as an Enumeration of * Header objects. */ @Override public Enumeration<Header> getMatchingHeaders(String[] names) throws MessagingException { return headers.getMatchingHeaders(names); } /** * Return non-matching headers from this Message as an * Enumeration of Header objects. */ @Override public Enumeration<Header> getNonMatchingHeaders(String[] names) throws MessagingException { return headers.getNonMatchingHeaders(names); } /** * Add a header line to this body part */ @Override public void addHeaderLine(String line) throws MessagingException { headers.addHeaderLine(line); } /** * Get all header lines as an Enumeration of Strings. A Header * line is a raw RFC 822 header line, containing both the "name" * and "value" field. */ @Override public Enumeration<String> getAllHeaderLines() throws MessagingException { return headers.getAllHeaderLines(); } /** * Get matching header lines as an Enumeration of Strings. * A Header line is a raw RFC 822 header line, containing both * the "name" and "value" field. */ @Override public Enumeration<String> getMatchingHeaderLines(String[] names) throws MessagingException { return headers.getMatchingHeaderLines(names); } /** * Get non-matching header lines as an Enumeration of Strings. * A Header line is a raw RFC 822 header line, containing both * the "name" and "value" field. */ @Override public Enumeration<String> getNonMatchingHeaderLines(String[] names) throws MessagingException { return headers.getNonMatchingHeaderLines(names); } /** * Examine the content of this body part and update the appropriate * MIME headers. Typical headers that get set here are * <code>Content-Type</code> and <code>Content-Transfer-Encoding</code>. * Headers might need to be updated in two cases: * * <br> * - A message being crafted by a mail application will certainly * need to activate this method at some point to fill up its internal * headers. * * <br> * - A message read in from a Store will have obtained * all its headers from the store, and so doesn't need this. * However, if this message is editable and if any edits have * been made to either the content or message structure, we might * need to resync our headers. * * <br> * In both cases this method is typically called by the * <code>Message.saveChanges</code> method. <p> * * If the {@link #cachedContent} field is not null (that is, * it references a Multipart or Message object), then * that object is used to set a new DataHandler, any * stream data used to create this object is discarded, * and the {@link #cachedContent} field is cleared. * * @exception MessagingException for failures */ protected void updateHeaders() throws MessagingException { updateHeaders(this); /* * If we've cached a Multipart or Message object then * we're now committed to using this instance of the * object and we discard any stream data used to create * this object. */ if (cachedContent != null) { dh = new DataHandler(cachedContent, getContentType()); cachedContent = null; content = null; if (contentStream != null) { try { contentStream.close(); } catch (IOException ioex) { } // nothing to do } contentStream = null; } } ///////////////////////////////////////////////////////////// // Package private convenience methods to share code among // // MimeMessage and MimeBodyPart // ///////////////////////////////////////////////////////////// static boolean isMimeType(MimePart part, String mimeType) throws MessagingException { // XXX - lots of room for optimization here! String type = part.getContentType(); try { return new ContentType(type).match(mimeType); } catch (ParseException ex) { // we only need the type and subtype so throw away the rest try { int i = type.indexOf(';'); if (i > 0) return new ContentType(type.substring(0, i)).match(mimeType); } catch (ParseException pex2) { } return type.equalsIgnoreCase(mimeType); } } static void setText(MimePart part, String text, String charset, String subtype) throws MessagingException { if (charset == null) { if (MimeUtility.checkAscii(text) != MimeUtility.ALL_ASCII) charset = MimeUtility.getDefaultMIMECharset(); else charset = "us-ascii"; } // XXX - should at least ensure that subtype is an atom part.setContent(text, "text/" + subtype + "; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME)); } static String getDisposition(MimePart part) throws MessagingException { String s = part.getHeader("Content-Disposition", null); if (s == null) return null; ContentDisposition cd = new ContentDisposition(s); return cd.getDisposition(); } static void setDisposition(MimePart part, String disposition) throws MessagingException { if (disposition == null) part.removeHeader("Content-Disposition"); else { String s = part.getHeader("Content-Disposition", null); if (s != null) { /* A Content-Disposition header already exists .. * * Override disposition, but attempt to retain * existing disposition parameters */ ContentDisposition cd = new ContentDisposition(s); cd.setDisposition(disposition); disposition = cd.toString(); } part.setHeader("Content-Disposition", disposition); } } static String getDescription(MimePart part) throws MessagingException { String rawvalue = part.getHeader("Content-Description", null); if (rawvalue == null) return null; try { return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); } catch (UnsupportedEncodingException ex) { return rawvalue; } } static void setDescription(MimePart part, String description, String charset) throws MessagingException { if (description == null) { part.removeHeader("Content-Description"); return; } try { part.setHeader("Content-Description", MimeUtility.fold(21, MimeUtility.encodeText(description, charset, null))); } catch (UnsupportedEncodingException uex) { throw new MessagingException("Encoding error", uex); } } static String getFileName(MimePart part) throws MessagingException { String filename = null; String s = part.getHeader("Content-Disposition", null); if (s != null) { // Parse the header .. ContentDisposition cd = new ContentDisposition(s); filename = cd.getParameter("filename"); } if (filename == null) { // Still no filename ? Try the "name" ContentType parameter s = part.getHeader("Content-Type", null); s = MimeUtil.cleanContentType(part, s); if (s != null) { try { ContentType ct = new ContentType(s); filename = ct.getParameter("name"); } catch (ParseException pex) { } // ignore it } } if (decodeFileName && filename != null) { try { filename = MimeUtility.decodeText(filename); } catch (UnsupportedEncodingException ex) { throw new MessagingException("Can't decode filename", ex); } } return filename; } static void setFileName(MimePart part, String name) throws MessagingException { if (encodeFileName && name != null) { try { name = MimeUtility.encodeText(name); } catch (UnsupportedEncodingException ex) { throw new MessagingException("Can't encode filename", ex); } } // Set the Content-Disposition "filename" parameter String s = part.getHeader("Content-Disposition", null); ContentDisposition cd = new ContentDisposition(s == null ? Part.ATTACHMENT : s); // ensure that the filename is encoded if necessary String charset = MimeUtility.getDefaultMIMECharset(); ParameterList p = cd.getParameterList(); if (p == null) { p = new ParameterList(); cd.setParameterList(p); } if (encodeFileName) p.setLiteral("filename", name); else p.set("filename", name, charset); part.setHeader("Content-Disposition", cd.toString()); /* * Also attempt to set the Content-Type "name" parameter, * to satisfy ancient MUAs. XXX - This is not RFC compliant. */ if (setContentTypeFileName) { s = part.getHeader("Content-Type", null); s = MimeUtil.cleanContentType(part, s); if (s != null) { try { ContentType cType = new ContentType(s); // ensure that the filename is encoded if necessary p = cType.getParameterList(); if (p == null) { p = new ParameterList(); cType.setParameterList(p); } if (encodeFileName) p.setLiteral("name", name); else p.set("name", name, charset); part.setHeader("Content-Type", cType.toString()); } catch (ParseException pex) { } // ignore it } } } static String[] getContentLanguage(MimePart part) throws MessagingException { String s = part.getHeader("Content-Language", null); if (s == null) return null; // Tokenize the header to obtain the Language-tags (skip comments) HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); List<String> v = new ArrayList<>(); HeaderTokenizer.Token tk; int tkType; while (true) { tk = h.next(); // get a language-tag tkType = tk.getType(); if (tkType == HeaderTokenizer.Token.EOF) break; // done else if (tkType == HeaderTokenizer.Token.ATOM) v.add(tk.getValue()); else // invalid token, skip it. continue; } if (v.isEmpty()) return null; String[] language = new String[v.size()]; v.toArray(language); return language; } static void setContentLanguage(MimePart part, String[] languages) throws MessagingException { StringBuilder sb = new StringBuilder(languages[0]); int len = "Content-Language".length() + 2 + languages[0].length(); for (int i = 1; i < languages.length; i++) { sb.append(','); len++; if (len > 76) { sb.append("\r\n\t"); len = 8; } sb.append(languages[i]); len += languages[i].length(); } part.setHeader("Content-Language", sb.toString()); } static String getEncoding(MimePart part) throws MessagingException { String s = part.getHeader("Content-Transfer-Encoding", null); if (s == null) return null; s = s.trim(); // get rid of trailing spaces if (s.length() == 0) return null; // quick check for known values to avoid unnecessary use // of tokenizer. if (s.equalsIgnoreCase("7bit") || s.equalsIgnoreCase("8bit") || s.equalsIgnoreCase("quoted-printable") || s.equalsIgnoreCase("binary") || s.equalsIgnoreCase("base64")) return s; // Tokenize the header to obtain the encoding (skip comments) HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); HeaderTokenizer.Token tk; int tkType; for (;;) { tk = h.next(); // get a token tkType = tk.getType(); if (tkType == HeaderTokenizer.Token.EOF) break; // done else if (tkType == HeaderTokenizer.Token.ATOM) return tk.getValue(); else // invalid token, skip it. continue; } return s; } static void setEncoding(MimePart part, String encoding) throws MessagingException { part.setHeader("Content-Transfer-Encoding", encoding); } /** * Restrict the encoding to values allowed for the * Content-Type of the specified MimePart. Returns * either the original encoding or null. */ static String restrictEncoding(MimePart part, String encoding) throws MessagingException { if (!ignoreMultipartEncoding || encoding == null) return encoding; if (encoding.equalsIgnoreCase("7bit") || encoding.equalsIgnoreCase("8bit") || encoding.equalsIgnoreCase("binary")) return encoding; // these encodings are always valid String type = part.getContentType(); if (type == null) return encoding; try { /* * multipart and message types aren't allowed to have * encodings except for the three mentioned above. * If it's one of these types, ignore the encoding. */ ContentType cType = new ContentType(type); if (cType.match("multipart/*")) return null; if (cType.match("message/*") && !PropUtil.getBooleanSystemProperty("mail.mime.allowencodedmessages", false)) return null; } catch (ParseException pex) { // ignore it } return encoding; } static void updateHeaders(MimePart part) throws MessagingException { DataHandler dh = part.getDataHandler(); if (dh == null) // Huh ? return; try { String type = dh.getContentType(); boolean composite = false; boolean needCTHeader = part.getHeader("Content-Type") == null; ContentType cType = new ContentType(type); /* * If this is a multipart, give sub-parts a chance to * update their headers. Even though the data for this * multipart may have come from a stream, one of the * sub-parts may have been updated. */ if (cType.match("multipart/*")) { // If multipart, recurse composite = true; Object o; if (part instanceof MimeBodyPart) { MimeBodyPart mbp = (MimeBodyPart) part; o = mbp.cachedContent != null ? mbp.cachedContent : dh.getContent(); } else if (part instanceof MimeMessage) { MimeMessage msg = (MimeMessage) part; o = msg.cachedContent != null ? msg.cachedContent : dh.getContent(); } else o = dh.getContent(); if (o instanceof MimeMultipart) ((MimeMultipart) o).updateHeaders(); else throw new MessagingException("MIME part of type \"" + type + "\" contains object of type " + o.getClass().getName() + " instead of MimeMultipart"); } else if (cType.match("message/rfc822")) { composite = true; // XXX - call MimeMessage.updateHeaders()? } /* * If this is our own MimePartDataHandler, we can't update any * of the headers. * * If this is a MimePartDataHandler coming from another part, * we need to copy over the content headers from the other part. * Note that the MimePartDataHandler still refers to the original * data and the original MimePart. */ if (dh instanceof MimePartDataHandler) { MimePartDataHandler mdh = (MimePartDataHandler) dh; MimePart mpart = mdh.getPart(); if (mpart != part) { if (needCTHeader) part.setHeader("Content-Type", mpart.getContentType()); // XXX - can't change the encoding of the data from the // other part without decoding and reencoding it, so // we just force it to match the original, but if the // original has no encoding we'll consider reencoding it String enc = mpart.getEncoding(); if (enc != null) { setEncoding(part, enc); return; } } else return; } // Content-Transfer-Encoding, but only if we don't // already have one if (!composite) { // not allowed on composite parts if (part.getHeader("Content-Transfer-Encoding") == null) setEncoding(part, MimeUtility.getEncoding(dh)); if (needCTHeader && setDefaultTextCharset && cType.match("text/*") && cType.getParameter("charset") == null) { /* * Set a default charset for text parts. * We really should examine the data to determine * whether or not it's all ASCII, but that's too * expensive so we make an assumption: If we * chose 7bit encoding for this data, it's probably * ASCII. (MimeUtility.getEncoding will choose * 7bit only in this case, but someone might've * set the Content-Transfer-Encoding header manually.) */ String charset; String enc = part.getEncoding(); if (enc != null && enc.equalsIgnoreCase("7bit")) charset = "us-ascii"; else charset = MimeUtility.getDefaultMIMECharset(); cType.setParameter("charset", charset); type = cType.toString(); } } // Now, let's update our own headers ... // Content-type, but only if we don't already have one if (needCTHeader) { /* * Pull out "filename" from Content-Disposition, and * use that to set the "name" parameter. This is to * satisfy older MUAs (DtMail, Roam and probably * a bunch of others). */ if (setContentTypeFileName) { String s = part.getHeader("Content-Disposition", null); if (s != null) { // Parse the header .. ContentDisposition cd = new ContentDisposition(s); String filename = cd.getParameter("filename"); if (filename != null) { ParameterList p = cType.getParameterList(); if (p == null) { p = new ParameterList(); cType.setParameterList(p); } if (encodeFileName) p.setLiteral("name", MimeUtility.encodeText(filename)); else p.set("name", filename, MimeUtility.getDefaultMIMECharset()); type = cType.toString(); } } } part.setHeader("Content-Type", type); } } catch (IOException ex) { throw new MessagingException("IOException updating headers", ex); } } static void invalidateContentHeaders(MimePart part) throws MessagingException { part.removeHeader("Content-Type"); part.removeHeader("Content-Transfer-Encoding"); } static void writeTo(MimePart part, OutputStream os, String[] ignoreList) throws IOException, MessagingException { // see if we already have a LOS LineOutputStream los = null; if (os instanceof LineOutputStream) { los = (LineOutputStream) os; } else { los = new LineOutputStream(os, allowutf8); } // First, write out the header Enumeration<String> hdrLines = part.getNonMatchingHeaderLines(ignoreList); while (hdrLines.hasMoreElements()) los.writeln(hdrLines.nextElement()); // The CRLF separator between header and content los.writeln(); // Finally, the content. Encode if required. // XXX: May need to account for ESMTP ? InputStream is = null; byte[] buf = null; try { /* * If the data for this part comes from a stream, * and is already encoded, * just copy it to the output stream without decoding * and reencoding it. */ DataHandler dh = part.getDataHandler(); if (dh instanceof MimePartDataHandler) { MimePartDataHandler mpdh = (MimePartDataHandler) dh; MimePart mpart = mpdh.getPart(); if (mpart.getEncoding() != null) is = mpdh.getContentStream(); } if (is != null) { // now copy the data to the output stream buf = new byte[8192]; int len; while ((len = is.read(buf)) > 0) os.write(buf, 0, len); } else { os = MimeUtility.encode(os, restrictEncoding(part, part.getEncoding())); part.getDataHandler().writeTo(os); } } finally { if (is != null) is.close(); buf = null; } os.flush(); // Needed to complete encoding } /** * A special DataHandler used only as a marker to indicate that * the source of the data is a MimePart (that is, a byte array * or a stream). This prevents updateHeaders from trying to * change the headers for such data. In particular, the original * Content-Transfer-Encoding for the data must be preserved. * Otherwise the data would need to be decoded and reencoded. */ static class MimePartDataHandler extends DataHandler { MimePart part; public MimePartDataHandler(MimePart part) { super(new MimePartDataSource(part)); this.part = part; } InputStream getContentStream() throws MessagingException { InputStream is = null; if (part instanceof MimeBodyPart) { MimeBodyPart mbp = (MimeBodyPart) part; is = mbp.getContentStream(); } else if (part instanceof MimeMessage) { MimeMessage msg = (MimeMessage) part; is = msg.getContentStream(); } return is; } MimePart getPart() { return part; } } }