You are viewing a plain text version of this content. The canonical link for it is here.
Posted to scm@geronimo.apache.org by jl...@apache.org on 2022/05/03 12:22:12 UTC

svn commit: r1900504 [10/22] - in /geronimo/specs/trunk: ./ geronimo-activation_2.0_spec/ geronimo-activation_2.0_spec/src/ geronimo-activation_2.0_spec/src/main/ geronimo-activation_2.0_spec/src/main/java/ geronimo-activation_2.0_spec/src/main/java/ja...

Added: geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MailDateFormat.java
URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MailDateFormat.java?rev=1900504&view=auto
==============================================================================
--- geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MailDateFormat.java (added)
+++ geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MailDateFormat.java Tue May  3 12:22:08 2022
@@ -0,0 +1,700 @@
+/*
+ * 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 jakarta.mail.internet;
+
+import java.text.DateFormatSymbols;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Formats ths date as specified by
+ * draft-ietf-drums-msg-fmt-08 dated January 26, 2000
+ * which supercedes RFC822.
+ * <p/>
+ * <p/>
+ * The format used is <code>EEE, d MMM yyyy HH:mm:ss Z</code> and
+ * locale is always US-ASCII.
+ *
+ * @version $Rev$ $Date$
+ */
+public class MailDateFormat extends SimpleDateFormat {
+	
+	private static final long serialVersionUID = -8148227605210628779L;
+	
+    public MailDateFormat() {
+        super("EEE, d MMM yyyy HH:mm:ss Z (z)", Locale.US);
+    }
+
+    @Override
+    public StringBuffer format(final Date date, final StringBuffer buffer, final FieldPosition position) {
+        return super.format(date, buffer, position);
+    }
+
+    /**
+     * Parse a Mail date into a Date object.  This uses fairly 
+     * lenient rules for the format because the Mail standards 
+     * for dates accept multiple formats.
+     * 
+     * @param string   The input string.
+     * @param position The position argument.
+     * 
+     * @return The Date object with the information inside. 
+     */
+    @Override
+    public Date parse(final String string, final ParsePosition position) {
+        final MailDateParser parser = new MailDateParser(string, position);
+        try {
+            return parser.parse(isLenient()); 
+        } catch (final ParseException e) {
+            e.printStackTrace(); 
+            // just return a null for any parsing errors 
+            return null; 
+        }
+    }
+
+    /**
+     * The calendar cannot be set
+     * @param calendar
+     * @throws UnsupportedOperationException
+     */
+    @Override
+    public void setCalendar(final Calendar calendar) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * The format cannot be set
+     * @param format
+     * @throws UnsupportedOperationException
+     */
+    @Override
+    public void setNumberFormat(final NumberFormat format) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * This method always throws an UnsupportedOperationException and
+     * should not be used because RFC 2822 mandates a specific pattern.
+     *
+     * @throws UnsupportedOperationException if this method is invoked
+     * @since JavaMail 1.6
+     */
+    @Override
+    public void applyLocalizedPattern(String pattern) {
+        throw new UnsupportedOperationException("Method "
+                + "applyLocalizedPattern() shouldn't be called");
+    }
+
+    /**
+     * This method always throws an UnsupportedOperationException and
+     * should not be used because RFC 2822 mandates a specific pattern.
+     *
+     * @throws UnsupportedOperationException if this method is invoked
+     * @since JavaMail 1.6
+     */
+    @Override
+    public void applyPattern(String pattern) {
+        throw new UnsupportedOperationException("Method "
+                + "applyPattern() shouldn't be called");
+    }
+
+    /**
+     * This method always throws an UnsupportedOperationException and
+     * should not be used because RFC 2822 mandates another strategy
+     * for interpreting 2-digits years.
+     *
+     * @return the start of the 100-year period into which two digit
+     * years are parsed
+     * @throws UnsupportedOperationException if this method is invoked
+     * @since JavaMail 1.6
+     */
+    @Override
+    public Date get2DigitYearStart() {
+        throw new UnsupportedOperationException("Method "
+                + "get2DigitYearStart() shouldn't be called");
+    }
+
+    /**
+     * This method always throws an UnsupportedOperationException and
+     * should not be used because RFC 2822 mandates another strategy
+     * for interpreting 2-digits years.
+     *
+     * @throws UnsupportedOperationException if this method is invoked
+     * @since JavaMail 1.6
+     */
+    @Override
+    public void set2DigitYearStart(Date startDate) {
+        throw new UnsupportedOperationException("Method "
+                + "set2DigitYearStart() shouldn't be called");
+    }
+
+    /**
+     * This method always throws an UnsupportedOperationException and
+     * should not be used because RFC 2822 mandates specific date
+     * format symbols.
+     *
+     * @throws UnsupportedOperationException if this method is invoked
+     * @since JavaMail 1.6
+     */
+    @Override
+    public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
+        throw new UnsupportedOperationException("Method "
+                + "setDateFormatSymbols() shouldn't be called");
+    }
+
+    /**
+     * Overrides Cloneable.
+     *
+     * @return a clone of this instance
+     * @since JavaMail 1.6
+     */
+    @Override
+    public MailDateFormat clone() {
+        return (MailDateFormat) super.clone();
+    }
+
+
+    // utility class for handling date parsing issues 
+    class MailDateParser {
+        // our list of defined whitespace characters 
+        static final String whitespace = " \t\r\n"; 
+        
+        // current parsing position 
+        int current; 
+        // our end parsing position 
+        int endOffset; 
+        // the date source string 
+        String source; 
+        // The parsing position. We update this as we move along and 
+        // also for any parsing errors 
+        ParsePosition pos; 
+        
+        public MailDateParser(final String source, final ParsePosition pos) 
+        {
+            this.source = source; 
+            this.pos = pos; 
+            // we start using the providing parsing index. 
+            this.current = pos.getIndex(); 
+            this.endOffset = source.length(); 
+        }
+        
+        /**
+         * Parse the timestamp, returning a date object. 
+         * 
+         * @param lenient The lenient setting from the Formatter object.
+         * 
+         * @return A Date object based off of parsing the date string.
+         * @exception ParseException
+         */
+        public Date parse(final boolean lenient) throws ParseException {
+            // we just skip over any next date format, which means scanning ahead until we
+            // find the first numeric character 
+            locateNumeric(); 
+            // the day can be either 1 or two digits 
+            final int day = parseNumber(1, 2); 
+            // step over the delimiter 
+            skipDateDelimiter(); 
+            // parse off the month (which is in character format) 
+            final int month = parseMonth(); 
+            // step over the delimiter 
+            skipDateDelimiter(); 
+            // now pull of the year, which can be either 2-digit or 4-digit 
+            final int year = parseYear(); 
+            // white space is required here 
+            skipRequiredWhiteSpace(); 
+            // accept a 1 or 2 digit hour 
+            final int hour = parseNumber(1, 2);
+            skipRequiredChar(':'); 
+            // the minutes must be two digit 
+            final int minutes = parseNumber(2, 2);
+            
+            // the seconds are optional, but the ":" tells us if they are to 
+            // be expected. 
+            int seconds = 0; 
+            if (skipOptionalChar(':')) {
+                seconds = parseNumber(2, 2); 
+            }
+            // skip over the white space 
+            skipWhiteSpace(); 
+            // and finally the timezone information 
+            final int offset = parseTimeZone(); 
+            
+            // set the index of how far we've parsed this 
+            pos.setIndex(current);
+            
+            // create a calendar for creating the date 
+            final Calendar greg = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 
+            // we inherit the leniency rules 
+            greg.setLenient(lenient);
+            greg.set(year, month, day, hour, minutes, seconds); 
+            // now adjust by the offset.  This seems a little strange, but we  
+            // need to negate the offset because this is a UTC calendar, so we need to 
+            // apply the reverse adjustment.  for example, for the EST timezone, the offset 
+            // value will be -300 (5 hours).  If the time was 15:00:00, the UTC adjusted time 
+            // needs to be 20:00:00, so we subract -300 minutes. 
+            greg.add(Calendar.MINUTE, -offset); 
+            // now return this timestamp. 
+            return greg.getTime(); 
+        }
+        
+        
+        /**
+         * Skip over a position where there's a required value 
+         * expected. 
+         * 
+         * @param ch     The required character.
+         * 
+         * @exception ParseException
+         */
+        private void skipRequiredChar(final char ch) throws ParseException {
+            if (current >= endOffset) {
+                parseError("Delimiter '" + ch + "' expected"); 
+            }
+            if (source.charAt(current) != ch) {
+                parseError("Delimiter '" + ch + "' expected"); 
+            }
+            current++; 
+        }
+        
+        
+        /**
+         * Skip over a position where iff the position matches the
+         * character
+         * 
+         * @param ch     The required character.
+         * 
+         * @return true if the character was there, false otherwise.
+         * @exception ParseException
+         */
+        private boolean skipOptionalChar(final char ch) {
+            if (current >= endOffset) {
+                return false; 
+            }
+            if (source.charAt(current) != ch) {
+                return false; 
+            }
+            current++; 
+            return true; 
+        }
+        
+        
+        /**
+         * Skip over any white space characters until we find 
+         * the next real bit of information.  Will scan completely to the 
+         * end, if necessary. 
+         */
+        private void skipWhiteSpace() {
+            while (current < endOffset) {
+                // if this is not in the white space list, then success. 
+                if (whitespace.indexOf(source.charAt(current)) < 0) {
+                    return; 
+                }
+                current++; 
+            }
+            
+            // everything used up, just return 
+        }
+        
+        
+        /**
+         * Skip over any non-white space characters until we find 
+         * either a whitespace char or the end of the data.
+         */
+        private void skipNonWhiteSpace() {
+            while (current < endOffset) {
+                // if this is not in the white space list, then success. 
+                if (whitespace.indexOf(source.charAt(current)) >= 0) {
+                    return; 
+                }
+                current++; 
+            }
+            
+            // everything used up, just return 
+        }
+        
+        
+        /**
+         * Skip over any white space characters until we find 
+         * the next real bit of information.  Will scan completely to the 
+         * end, if necessary. 
+         */
+        private void skipRequiredWhiteSpace() throws ParseException {
+            final int start = current; 
+            
+            while (current < endOffset) {
+                // if this is not in the white space list, then success. 
+                if (whitespace.indexOf(source.charAt(current)) < 0) {
+                    // we must have at least one white space character 
+                    if (start == current) {
+                        parseError("White space character expected"); 
+                    }
+                    return; 
+                }
+                current++; 
+            }
+            // everything used up, just return, but make sure we had at least one  
+            // white space
+            if (start == current) {
+                parseError("White space character expected"); 
+            }
+        }
+        
+        private void parseError(final String message) throws ParseException {
+            // we've got an error, set the index to the end. 
+            pos.setErrorIndex(current);
+            throw new ParseException(message, current); 
+        }
+        
+        
+        /**
+         * Locate an expected numeric field. 
+         * 
+         * @exception ParseException
+         */
+        private void locateNumeric() throws ParseException {
+            while (current < endOffset) {
+                // found a digit?  we're done
+                if (Character.isDigit(source.charAt(current))) {
+                    return; 
+                }
+                current++; 
+            }
+            // we've got an error, set the index to the end. 
+            parseError("Number field expected"); 
+        }
+        
+        
+        /**
+         * Parse out an expected numeric field. 
+         * 
+         * @param minDigits The minimum number of digits we expect in this filed.
+         * @param maxDigits The maximum number of digits expected.  Parsing will
+         *                  stop at the first non-digit character.  An exception will
+         *                  be thrown if the field contained more than maxDigits
+         *                  in it.
+         * 
+         * @return The parsed numeric value. 
+         * @exception ParseException
+         */
+        private int parseNumber(final int minDigits, final int maxDigits) throws ParseException {
+            final int start = current; 
+            int accumulator = 0; 
+            while (current < endOffset) {
+                final char ch = source.charAt(current); 
+                // if this is not a digit character, then quit
+                if (!Character.isDigit(ch)) {
+                    break; 
+                }
+                // add the digit value into the accumulator 
+                accumulator = accumulator * 10 + Character.digit(ch, 10); 
+                current++; 
+            }
+            
+            final int fieldLength = current - start; 
+            if (fieldLength < minDigits || fieldLength > maxDigits) {
+                parseError("Invalid number field"); 
+            }
+            
+            return accumulator; 
+        }
+        
+        /**
+         * Skip a delimiter between the date portions of the
+         * string.  The IMAP internal date format uses "-", so 
+         * we either accept a single "-" or any number of white
+         * space characters (at least one required). 
+         * 
+         * @exception ParseException
+         */
+        private void skipDateDelimiter() throws ParseException {
+            if (current >= endOffset) {
+                parseError("Invalid date field delimiter"); 
+            }
+            
+            if (source.charAt(current) == '-') {
+                current++; 
+            }
+            else {
+                // must be at least a single whitespace character 
+                skipRequiredWhiteSpace(); 
+            }
+        }
+        
+        
+        /**
+         * Parse a character month name into the date month 
+         * offset.
+         * 
+         * @return 
+         * @exception ParseException
+         */
+        private int parseMonth() throws ParseException {
+            if ((endOffset - current) < 3) {
+                parseError("Invalid month"); 
+            }
+            
+            int monthOffset = 0; 
+            final String month = source.substring(current, current + 3).toLowerCase();
+            
+            if (month.equals("jan")) {
+                monthOffset = 0; 
+            }
+            else if (month.equals("feb")) {
+                monthOffset = 1; 
+            }
+            else if (month.equals("mar")) {
+                monthOffset = 2; 
+            }
+            else if (month.equals("apr")) {
+                monthOffset = 3; 
+            }
+            else if (month.equals("may")) {
+                monthOffset = 4; 
+            }
+            else if (month.equals("jun")) {
+                monthOffset = 5; 
+            }
+            else if (month.equals("jul")) {
+                monthOffset = 6; 
+            }
+            else if (month.equals("aug")) {
+                monthOffset = 7; 
+            }
+            else if (month.equals("sep")) {
+                monthOffset = 8; 
+            }
+            else if (month.equals("oct")) {
+                monthOffset = 9; 
+            }
+            else if (month.equals("nov")) {
+                monthOffset = 10; 
+            }
+            else if (month.equals("dec")) {
+                monthOffset = 11; 
+            }
+            else {
+                parseError("Invalid month"); 
+            }
+            
+            // ok, this is valid.  Update the position and return it 
+            current += 3;
+            return monthOffset; 
+        }
+        
+        /**
+         * Parse off a year field that might be expressed as 
+         * either 2 or 4 digits. 
+         * 
+         * @return The numeric value of the year. 
+         * @exception ParseException
+         */
+        private int parseYear() throws ParseException {
+            // the year is between 2 to 4 digits 
+            int year = parseNumber(2, 4); 
+            
+            // the two digit years get some sort of adjustment attempted. 
+            if (year < 50) {
+                year += 2000; 
+            }
+            else if (year < 100) {
+                year += 1990; 
+            }
+            return year; 
+        }
+        
+        
+        /**
+         * Parse all of the different timezone options. 
+         * 
+         * @return The timezone offset.
+         * @exception ParseException
+         */
+        private int parseTimeZone() throws ParseException {
+            if (current >= endOffset) {
+                parseError("Missing time zone"); 
+            }
+            
+            // get the first non-blank. If this is a sign character, this 
+            // is a zone offset.  
+            final char sign = source.charAt(current); 
+            
+            if (sign == '-' || sign == '+') {
+                // need to step over the sign character 
+                current++; 
+                // a numeric timezone is always a 4 digit number, but 
+                // expressed as minutes/seconds.  I'm too lazy to write a 
+                // different parser that will bound on just a couple of characters, so 
+                // we'll grab this as a single value and adjust     
+                final int zoneInfo = parseNumber(4, 4);
+                
+                int offset = (zoneInfo / 100) * 60 + (zoneInfo % 100); 
+                // negate this, if we have a negativeo offset 
+                if (sign == '-') {
+                    offset = -offset; 
+                }
+                return offset; 
+            }
+            else {
+                // need to parse this out using the obsolete zone names.  This will be 
+                // either a 3-character code (defined set), or a single character military 
+                // zone designation. 
+                final int start = current; 
+                skipNonWhiteSpace(); 
+                final String name = source.substring(start, current).toUpperCase(); 
+                
+                if (name.length() == 1) {
+                    return militaryZoneOffset(name); 
+                }
+                else if (name.length() <= 3) {
+                    return namedZoneOffset(name); 
+                }
+                else {
+                    parseError("Invalid time zone"); 
+                }
+                return 0; 
+            }
+        }
+        
+        
+        /**
+         * Parse the obsolete mail timezone specifiers. The
+         * allowed set of timezones are terribly US centric. 
+         * That's the spec.  The preferred timezone form is 
+         * the +/-mmss form. 
+         * 
+         * @param name   The input name.
+         * 
+         * @return The standard timezone offset for the specifier.
+         * @exception ParseException
+         */
+        private int namedZoneOffset(final String name) throws ParseException {
+            
+            // NOTE:  This is "UT", NOT "UTC"
+            if (name.equals("UT")) {
+                return 0; 
+            }
+            else if (name.equals("GMT")) {
+                return 0; 
+            }
+            else if (name.equals("EST")) {
+                return -300; 
+            }
+            else if (name.equals("EDT")) {
+                return -240; 
+            }
+            else if (name.equals("CST")) {
+                return -360; 
+            }
+            else if (name.equals("CDT")) {
+                return -300; 
+            }
+            else if (name.equals("MST")) {
+                return -420; 
+            }
+            else if (name.equals("MDT")) {
+                return -360; 
+            }
+            else if (name.equals("PST")) {
+                return -480; 
+            }
+            else if (name.equals("PDT")) {
+                return -420; 
+            }
+            else {
+                parseError("Invalid time zone"); 
+                return 0; 
+            }
+        }
+        
+        
+        /**
+         * Parse a single-character military timezone. 
+         * 
+         * @param name   The one-character name.
+         * 
+         * @return The offset corresponding to the military designation.
+         */
+        private int militaryZoneOffset(final String name) throws ParseException {
+            switch (Character.toUpperCase(name.charAt(0))) {
+                case 'A':
+                    return 60; 
+                case 'B':
+                    return 120; 
+                case 'C':
+                    return 180;
+                case 'D':
+                    return 240;
+                case 'E':
+                    return 300;
+                case 'F':
+                    return 360;
+                case 'G':
+                    return 420;
+                case 'H':
+                    return 480;
+                case 'I':
+                    return 540;
+                case 'K':
+                    return 600;
+                case 'L':
+                    return 660;
+                case 'M':
+                    return 720;
+                case 'N':
+                    return -60;
+                case 'O':
+                    return -120;
+                case 'P':
+                    return -180;
+                case 'Q':
+                    return -240;
+                case 'R':
+                    return -300;
+                case 'S':
+                    return -360;
+                case 'T':
+                    return -420;
+                case 'U':
+                    return -480;
+                case 'V':
+                    return -540;
+                case 'W':
+                    return -600;
+                case 'X':
+                    return -660;
+                case 'Y':
+                    return -720;
+                case 'Z':
+                    return 0;    
+                default:
+                    parseError("Invalid time zone");
+                    return 0; 
+            }
+        }
+    }
+}

Added: geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MimeBodyPart.java
URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MimeBodyPart.java?rev=1900504&view=auto
==============================================================================
--- geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MimeBodyPart.java (added)
+++ geronimo/specs/trunk/geronimo-jakartamail_2.1_spec/src/main/java/jakarta/mail/internet/MimeBodyPart.java Tue May  3 12:22:08 2022
@@ -0,0 +1,830 @@
+/*
+ * 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 jakarta.mail.internet;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Enumeration;
+
+import jakarta.activation.DataHandler;
+import jakarta.activation.FileDataSource;
+
+import jakarta.mail.*;
+import jakarta.mail.internet.HeaderTokenizer.Token;
+
+import org.apache.geronimo.mail.util.ASCIIUtil;
+import org.apache.geronimo.mail.util.SessionUtil;
+
+
+
+/**
+ * @version $Rev$ $Date$
+ */
+public class MimeBodyPart extends BodyPart implements MimePart {
+	 // constants for accessed properties
+    private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
+    private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
+    private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset";
+    private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename";
+
+    static final boolean cacheMultipart = SessionUtil.getBooleanProperty("mail.mime.cachemultipart", true);
+
+    /**
+     * The {@link DataHandler} for this Message's content.
+     */
+    protected DataHandler dh;
+    /**
+     * This message's content (unless sourced from a SharedInputStream).
+     */
+    
+    
+    /**
+     * If our content is a Multipart or 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;
+    
+    
+    protected byte content[];
+    /**
+     * If the data for this message was supplied by a {@link SharedInputStream}
+     * then this is another such stream representing the content of this message;
+     * if this field is non-null, then {@link #content} will be null.
+     */
+    protected InputStream contentStream;
+    /**
+     * This message's headers.
+     */
+    protected InternetHeaders headers;
+
+    public MimeBodyPart() {
+        headers = new InternetHeaders();
+    }
+
+    public MimeBodyPart(final InputStream in) throws MessagingException {
+        headers = new InternetHeaders(in);
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final byte[] buffer = new byte[1024];
+        int count;
+        try {
+            while((count = in.read(buffer, 0, 1024)) > 0) {
+                baos.write(buffer, 0, count);
+            }
+        } catch (final IOException e) {
+            throw new MessagingException(e.toString(),e);
+        }
+        content = baos.toByteArray();
+    }
+
+    public MimeBodyPart(final InternetHeaders headers, final byte[] content) throws MessagingException {
+        this.headers = headers;
+        this.content = content;
+    }
+
+    /**
+     * Return the content size of this message.  This is obtained
+     * either from the size of the content field (if available) or
+     * from the contentStream, IFF the contentStream returns a positive
+     * size.  Returns -1 if the size is not available.
+     *
+     * @return Size of the content in bytes.
+     * @exception MessagingException
+     */
+    public int getSize() throws MessagingException {
+        if (content != null) {
+            return content.length;
+        }
+        if (contentStream != null) {
+            try {
+                final int size = contentStream.available();
+                if (size > 0) {
+                    return size;
+                }
+            } catch (final IOException e) {
+            }
+        }
+        return -1;
+    }
+
+    public int getLineCount() throws MessagingException {
+        return -1;
+    }
+
+    public String getContentType() throws MessagingException {
+        String value = getSingleHeader("Content-Type");
+        if (value == null) {
+            value = "text/plain";
+        }
+        return value;
+    }
+
+    /**
+     * Tests to see if this message has a mime-type match with the
+     * given type name.
+     *
+     * @param type   The tested type name.
+     *
+     * @return If this is a type match on the primary and secondare portion of the types.
+     * @exception MessagingException
+     */
+    public boolean isMimeType(final String type) throws MessagingException {
+        return new ContentType(getContentType()).match(type);
+    }
+
+    /**
+     * Retrieve the message "Content-Disposition" header field.
+     * This value represents how the part should be represented to
+     * the user.
+     *
+     * @return The string value of the Content-Disposition field.
+     * @exception MessagingException
+     */
+    public String getDisposition() throws MessagingException {
+        final String disp = getSingleHeader("Content-Disposition");
+        if (disp != null) {
+            return new ContentDisposition(disp).getDisposition();
+        }
+        return null;
+    }
+
+    /**
+     * Set a new dispostion value for the "Content-Disposition" field.
+     * If the new value is null, the header is removed.
+     *
+     * @param disposition
+     *               The new disposition value.
+     *
+     * @exception MessagingException
+     */
+    public void setDisposition(final String disposition) throws MessagingException {
+        if (disposition == null) {
+            removeHeader("Content-Disposition");
+        }
+        else {
+            // the disposition has parameters, which we'll attempt to preserve in any existing header.
+            final String currentHeader = getSingleHeader("Content-Disposition");
+            if (currentHeader != null) {
+                final ContentDisposition content = new ContentDisposition(currentHeader);
+                content.setDisposition(disposition);
+                setHeader("Content-Disposition", content.toString());
+            }
+            else {
+                // set using the raw string.
+                setHeader("Content-Disposition", disposition);
+            }
+        }
+    }
+
+    /**
+     * Retrieves the current value of the "Content-Transfer-Encoding"
+     * header.  Returns null if the header does not exist.
+     *
+     * @return The current header value or null.
+     * @exception MessagingException
+     */
+    public String getEncoding() throws MessagingException {
+        // this might require some parsing to sort out.
+        final String encoding = getSingleHeader("Content-Transfer-Encoding");
+        if (encoding != null) {
+            // we need to parse this into ATOMs and other constituent parts.  We want the first
+            // ATOM token on the string.
+            final HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
+
+            final Token token = tokenizer.next();
+            while (token.getType() != Token.EOF) {
+                // if this is an ATOM type, return it.
+                if (token.getType() == Token.ATOM) {
+                    return token.getValue();
+                }
+            }
+            // not ATOMs found, just return the entire header value....somebody might be able to make sense of
+            // this.
+            return encoding;
+        }
+        // no header, nothing to return.
+        return null;
+    }
+
+
+    /**
+     * Retrieve the value of the "Content-ID" header.  Returns null
+     * if the header does not exist.
+     *
+     * @return The current header value or null.
+     * @exception MessagingException
+     */
+    public String getContentID() throws MessagingException {
+        return getSingleHeader("Content-ID");
+    }
+
+    public void setContentID(final String cid) throws MessagingException {
+        setOrRemoveHeader("Content-ID", cid);
+    }
+
+    public String getContentMD5() throws MessagingException {
+        return getSingleHeader("Content-MD5");
+    }
+
+    public void setContentMD5(final String md5) throws MessagingException {
+        setHeader("Content-MD5", md5);
+    }
+
+    public String[] getContentLanguage() throws MessagingException {
+        return getHeader("Content-Language");
+    }
+
+    public void setContentLanguage(final String[] languages) throws MessagingException {
+        if (languages == null) {
+            removeHeader("Content-Language");
+        } else if (languages.length == 1) {
+            setHeader("Content-Language", languages[0]);
+        } else {
+            final StringBuffer buf = new StringBuffer(languages.length * 20);
+            buf.append(languages[0]);
+            for (int i = 1; i < languages.length; i++) {
+                buf.append(',').append(languages[i]);
+            }
+            setHeader("Content-Language", buf.toString());
+        }
+    }
+
+    public String getDescription() throws MessagingException {
+        final String description = getSingleHeader("Content-Description");
+        if (description != null) {
+            try {
+                // this could be both folded and encoded.  Return this to usable form.
+                return MimeUtility.decodeText(MimeUtility.unfold(description));
+            } catch (final UnsupportedEncodingException e) {
+                // ignore
+            }
+        }
+        // return the raw version for any errors.
+        return description;
+    }
+
+    public void setDescription(final String description) throws MessagingException {
+        setDescription(description, null);
+    }
+
+    public void setDescription(final String description, final String charset) throws MessagingException {
+        if (description == null) {
+            removeHeader("Content-Description");
+        }
+        else {
+            try {
+                setHeader("Content-Description", MimeUtility.fold(21, MimeUtility.encodeText(description, charset, null)));
+            } catch (final UnsupportedEncodingException e) {
+                throw new MessagingException(e.getMessage(), e);
+            }
+        }
+    }
+
+    public String getFileName() throws MessagingException {
+        // see if there is a disposition.  If there is, parse off the filename parameter.
+        final String disposition = getSingleHeader("Content-Disposition");
+        String filename = null;
+
+        if (disposition != null) {
+            filename = new ContentDisposition(disposition).getParameter("filename");
+        }
+
+        // if there's no filename on the disposition, there might be a name parameter on a
+        // Content-Type header.
+        if (filename == null) {
+            final String type = getSingleHeader("Content-Type");
+            if (type != null) {
+                try {
+                    filename = new ContentType(type).getParameter("name");
+                } catch (final ParseException e) {
+                }
+            }
+        }
+        // if we have a name, we might need to decode this if an additional property is set.
+        if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) {
+            try {
+                filename = MimeUtility.decodeText(filename);
+            } catch (final UnsupportedEncodingException e) {
+                throw new MessagingException("Unable to decode filename", e);
+            }
+        }
+
+        return filename;
+    }
+
+
+    public void setFileName(String name) throws MessagingException {
+        // there's an optional session property that requests file name encoding...we need to process this before
+        // setting the value.
+        if (name != null && SessionUtil.getBooleanProperty(MIME_ENCODEFILENAME, false)) {
+            try {
+                name = MimeUtility.encodeText(name);
+            } catch (final UnsupportedEncodingException e) {
+                throw new MessagingException("Unable to encode filename", e);
+            }
+        }
+
+        // get the disposition string.
+        String disposition = getDisposition();
+        // if not there, then this is an attachment.
+        if (disposition == null) {
+            disposition = Part.ATTACHMENT;
+        }
+
+        // now create a disposition object and set the parameter.
+        final ContentDisposition contentDisposition = new ContentDisposition(disposition);
+        contentDisposition.setParameter("filename", name);
+
+        // serialize this back out and reset.
+        setHeader("Content-Disposition", contentDisposition.toString());
+
+        // The Sun implementation appears to update the Content-type name parameter too, based on
+        // another system property
+        if (SessionUtil.getBooleanProperty(MIME_SETCONTENTTYPEFILENAME, true)) {
+            final ContentType type = new ContentType(getContentType());
+            type.setParameter("name", name);
+            setHeader("Content-Type", type.toString());
+        }
+    }
+
+    public InputStream getInputStream() throws MessagingException, IOException {
+        return getDataHandler().getInputStream();
+    }
+
+    protected InputStream getContentStream() throws MessagingException {
+        if (contentStream != null) {
+            return contentStream;
+        }
+
+        if (content != null) {
+            return new ByteArrayInputStream(content);
+        } else {
+            throw new MessagingException("No content");
+        }
+    }
+
+    public InputStream getRawInputStream() throws MessagingException {
+        return getContentStream();
+    }
+
+    public synchronized DataHandler getDataHandler() throws MessagingException {
+        if (dh == null) {
+            dh = new DataHandler(new MimePartDataSource(this));
+        }
+        return dh;
+    }
+    
+    public Object getContent() throws MessagingException, IOException {
+        
+        if (cachedContent != null) {
+            return cachedContent;
+        }
+        
+        final Object c = getDataHandler().getContent();
+        
+        if (MimeBodyPart.cacheMultipart && (c instanceof Multipart || c instanceof Message) && (content != null || contentStream != null)) {
+            cachedContent = c;
+ 
+            if (c instanceof MimeMultipart) {
+                ((MimeMultipart) c).parse();
+            }
+        }
+        
+        return c;
+    }
+
+    public void setDataHandler(final DataHandler handler) throws MessagingException {
+        dh = handler;
+        // if we have a handler override, then we need to invalidate any content
+        // headers that define the types.  This information will be derived from the
+        // data heander unless subsequently overridden.
+        removeHeader("Content-Type");
+        removeHeader("Content-Transfer-Encoding");
+        cachedContent = null;
+
+    }
+
+    public void setContent(final Object content, final String type) throws MessagingException {
+        // Multipart content needs to be handled separately.
+        if (content instanceof Multipart) {
+            setContent((Multipart)content);
+        }
+        else {
+            setDataHandler(new DataHandler(content, type));
+        }
+
+    }
+
+    public void setText(final String text) throws MessagingException {
+        setText(text, null);
+    }
+
+    public void setText(final String text, final String charset) throws MessagingException {
+        // the default subtype is plain text.
+        setText(text, charset, "plain");
+    }
+
+
+    public void setText(final String text, String charset, final String subtype) throws MessagingException {
+        // we need to sort out the character set if one is not provided.
+        if (charset == null) {
+            // if we have non us-ascii characters here, we need to adjust this.
+            if (!ASCIIUtil.isAscii(text)) {
+                charset = MimeUtility.getDefaultMIMECharset();
+            }
+            else {
+                charset = "us-ascii";
+            }
+        }
+        String actualSubtype = subtype;
+        if(actualSubtype == null || actualSubtype.isEmpty()) {
+            actualSubtype = "plain";
+        }
+        setContent(text, "text/" + actualSubtype + "; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME));
+    }
+
+    public void setContent(final Multipart part) throws MessagingException {
+        setDataHandler(new DataHandler(part, part.getContentType()));
+        part.setParent(this);
+    }
+
+    public void writeTo(final OutputStream out) throws IOException, MessagingException {
+        headers.writeTo(out, null);
+        // add the separater between the headers and the data portion.
+        out.write('\r');
+        out.write('\n');
+        // we need to process this using the transfer encoding type
+        final OutputStream encodingStream = MimeUtility.encode(out, getEncoding());
+        getDataHandler().writeTo(encodingStream);
+        encodingStream.flush();
+    }
+
+    public String[] getHeader(final String name) throws MessagingException {
+        return headers.getHeader(name);
+    }
+
+    public String getHeader(final String name, final String delimiter) throws MessagingException {
+        return headers.getHeader(name, delimiter);
+    }
+
+    public void setHeader(final String name, final String value) throws MessagingException {
+        headers.setHeader(name, value);
+    }
+
+    /**
+     * Conditionally set or remove a named header.  If the new value
+     * is null, the header is removed.
+     *
+     * @param name   The header name.
+     * @param value  The new header value.  A null value causes the header to be
+     *               removed.
+     *
+     * @exception MessagingException
+     */
+    private void setOrRemoveHeader(final String name, final String value) throws MessagingException {
+        if (value == null) {
+            headers.removeHeader(name);
+        }
+        else {
+            headers.setHeader(name, value);
+        }
+    }
+
+    public void addHeader(final String name, final String value) throws MessagingException {
+        headers.addHeader(name, value);
+    }
+
+    public void removeHeader(final String name) throws MessagingException {
+        headers.removeHeader(name);
+    }
+
+    public Enumeration<Header> getAllHeaders() throws MessagingException {
+        return headers.getAllHeaders();
+    }
+
+    public Enumeration<Header> getMatchingHeaders(final String[] name) throws MessagingException {
+        return headers.getMatchingHeaders(name);
+    }
+
+    public Enumeration<Header> getNonMatchingHeaders(final String[] name) throws MessagingException {
+        return headers.getNonMatchingHeaders(name);
+    }
+
+    public void addHeaderLine(final String line) throws MessagingException {
+        headers.addHeaderLine(line);
+    }
+
+    public Enumeration<String> getAllHeaderLines() throws MessagingException {
+        return headers.getAllHeaderLines();
+    }
+
+    public Enumeration<String> getMatchingHeaderLines(final String[] names) throws MessagingException {
+        return headers.getMatchingHeaderLines(names);
+    }
+
+    public Enumeration<String> getNonMatchingHeaderLines(final String[] names) throws MessagingException {
+        return headers.getNonMatchingHeaderLines(names);
+    }
+
+    protected void updateHeaders() throws MessagingException {
+        final DataHandler handler = getDataHandler();
+
+        try {
+            // figure out the content type.  If not set, we'll need to figure this out.
+            String type = dh.getContentType();
+            // parse this content type out so we can do matches/compares.
+            final ContentType contentType = new ContentType(type);
+            
+            // we might need to reconcile the content type and our explicitly set type
+            final String explicitType = getSingleHeader("Content-Type"); 
+            // is this a multipart content?
+            if (contentType.match("multipart/*")) {
+                // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
+                try {
+                    final MimeMultipart part = (MimeMultipart)handler.getContent();
+                    part.updateHeaders();
+                } catch (final ClassCastException e) {
+                    throw new MessagingException("Message content is not MimeMultipart", e);
+                }
+            }
+            else if (!contentType.match("message/rfc822")) {
+                // simple part, we need to update the header type information
+                // if no encoding is set yet, figure this out from the data handler.
+                if (getSingleHeader("Content-Transfer-Encoding") == null) {
+                    setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
+                }
+
+                // is a content type header set?  Check the property to see if we need to set this.
+                if (explicitType == null) {
+                    if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) {
+                        // is this a text type?  Figure out the encoding and make sure it is set.
+                        if (contentType.match("text/*")) {
+                            // the charset should be specified as a parameter on the MIME type.  If not there,
+                            // try to figure one out.
+                            if (contentType.getParameter("charset") == null) {
+
+                                final String encoding = getEncoding();
+                                // if we're sending this as 7-bit ASCII, our character set need to be
+                                // compatible.
+                                if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
+                                    contentType.setParameter("charset", "us-ascii");
+                                }
+                                else {
+                                    // get the global default.
+                                    contentType.setParameter("charset", MimeUtility.getDefaultMIMECharset());
+                                }
+                                // replace the datasource provided type 
+                                type = contentType.toString(); 
+                            }
+                        }
+                    }
+                }
+            }
+
+            // if we don't have a content type header, then create one.
+            if (explicitType == null) {
+                // get the disposition header, and if it is there, copy the filename parameter into the
+                // name parameter of the type.
+                final String disp = getHeader("Content-Disposition", null);
+                if (disp != null) {
+                    // parse up the string value of the disposition
+                    final ContentDisposition disposition = new ContentDisposition(disp);
+                    // now check for a filename value
+                    final String filename = disposition.getParameter("filename");
+                    // copy and rename the parameter, if it exists.
+                    if (filename != null) {
+                        contentType.setParameter("name", filename);
+                        // and update the string version 
+                        type = contentType.toString(); 
+                    }
+                }
+                // set the header with the updated content type information.
+                setHeader("Content-Type", type);
+            }
+            
+            
+            if (cachedContent != null) {
+                dh = new DataHandler(cachedContent, getContentType());
+                cachedContent = null;
+                content = null;
+                if (contentStream != null) {
+                    try {
+                        contentStream.close();
+                    } catch (final IOException ioex) {
+                        //np-op
+                    }
+                }
+                contentStream = null;
+            }
+
+        } catch (final IOException e) {
+            throw new MessagingException("Error updating message headers", e);
+        }
+    }
+
+    private String getSingleHeader(final String name) throws MessagingException {
+        final String[] values = getHeader(name);
+        if (values == null || values.length == 0) {
+            return null;
+        } else {
+            return values[0];
+        }
+    }
+
+
+    /**
+     * 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(final File file) throws IOException, MessagingException {
+                
+    	final FileDataSource dataSource = new FileDataSource(file);
+        setDataHandler(new DataHandler(dataSource));
+        setFileName(dataSource.getName());
+        
+        /* Since JavaMail 1.5:
+         An oversight when these methods were originally added.
+         Clearly attachments should set the disposition to ATTACHMENT.
+         */
+        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(final String file) throws IOException, MessagingException {
+
+        attachFile(new File(file));
+    }
+    
+    
+    
+    /**
+     * 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(final File file, final String contentType, final String encoding)
+                throws IOException, MessagingException {
+     
+        final FileDataSource dataSource = new EncodingAwareFileDataSource(file, contentType, encoding);
+        setDataHandler(new DataHandler(dataSource));
+        setFileName(dataSource.getName());
+               
+        /* Since JavaMail 1.5:
+         An oversight when these methods were originally added.
+         Clearly attachments should set the disposition to ATTACHMENT.
+         */
+        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(final String file, final String contentType, final String encoding)
+                throws IOException, MessagingException {
+        
+        attachFile(new File(file), contentType, encoding);
+    }
+
+
+    /**
+     * Save the body part content to a given target file.
+     *
+     * @param file   The File object used to store the information.
+     *
+     * @exception IOException
+     * @exception MessagingException
+     */
+    public void saveFile(final File file) throws IOException, MessagingException {
+    	final OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
+        // we need to read the data in to write it out (sigh).
+        final InputStream in = getInputStream();
+        try {
+            final byte[] buffer = new byte[8192];
+	        int length;
+	        while ((length = in.read(buffer)) > 0) {
+         		out.write(buffer, 0, length);
+            }
+        }
+        finally {
+            // make sure all of the streams are closed before we return
+            if (in != null) {
+                in.close();
+            }
+            if (out != null) {
+                out.close();
+            }
+        }
+    }
+
+
+    /**
+     * Save the body part content to a given target file.
+     *
+     * @param file   The file name used to store the information.
+     *
+     * @exception IOException
+     * @exception MessagingException
+     */
+    public void saveFile(final String file) throws IOException, MessagingException {
+        saveFile(new File(file));
+    }
+    
+    private static class EncodingAwareFileDataSource extends FileDataSource implements EncodingAware {
+        private final String contentType;
+        private final String encoding;
+
+        public EncodingAwareFileDataSource(final File file, final String contentType, final String encoding) {
+            super(file);
+            this.contentType = contentType;
+            this.encoding = encoding;
+        }
+
+        @Override
+        public String getContentType() {
+            return contentType == null ? super.getContentType() : contentType;
+        }
+
+        //this will be evaluated in MimeUtility.getEncoding(DataSource)
+        public String getEncoding() {
+            return encoding;
+        }
+    }
+}