You are viewing a plain text version of this content. The canonical link for it is here.
Posted to scm@geronimo.apache.org by rm...@apache.org on 2014/08/26 20:17:09 UTC

svn commit: r1620683 [6/17] - in /geronimo/specs/trunk: ./ geronimo-javamail_1.5_spec/ geronimo-javamail_1.5_spec/src/ geronimo-javamail_1.5_spec/src/main/ geronimo-javamail_1.5_spec/src/main/java/ geronimo-javamail_1.5_spec/src/main/java/javax/ geroni...

Added: geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/HeaderTokenizer.java
URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/HeaderTokenizer.java?rev=1620683&view=auto
==============================================================================
--- geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/HeaderTokenizer.java (added)
+++ geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/HeaderTokenizer.java Tue Aug 26 18:17:06 2014
@@ -0,0 +1,352 @@
+/*
+ * 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 javax.mail.internet;
+
+/**
+ * @version $Rev$ $Date$
+ */
+public class HeaderTokenizer {
+    public static class Token {
+        // Constant values from J2SE 1.4 API Docs (Constant values)
+        public static final int ATOM = -1;
+        public static final int COMMENT = -3;
+        public static final int EOF = -4;
+        public static final int QUOTEDSTRING = -2;
+        private final int _type;
+        private final String _value;
+
+        public Token(final int type, final String value) {
+            _type = type;
+            _value = value;
+        }
+
+        public int getType() {
+            return _type;
+        }
+
+        public String getValue() {
+            return _value;
+        }
+    }
+
+    private static final char NUL = '\0';
+    private static final Token EOF = new Token(Token.EOF, null);
+    // characters not allowed in MIME
+    public static final String MIME = "()<>@,;:\\\"\t []/?=";
+    // characters not allowed in RFC822
+    public static final String RFC822 = "()<>@,;:\\\"\t .[]";
+    private static final String WHITE = " \t\n\r";
+    private final String _delimiters;
+    private final String _header;
+    private final int _headerLength;
+    private final boolean _skip;
+    private int pos;
+
+    public HeaderTokenizer(final String header) {
+        this(header, RFC822);
+    }
+
+    public HeaderTokenizer(final String header, final String delimiters) {
+        this(header, delimiters, true);
+    }
+
+    public HeaderTokenizer(final String header,
+                           final String delimiters,
+                           final boolean skipComments) {
+        _skip = skipComments;
+        _header = header;
+        _delimiters = delimiters;
+        _headerLength=header.length();
+    }
+
+    //Return the rest of the Header.
+    //null is returned if we are already at end of header
+    public String getRemainder() {
+        
+        if(pos > _headerLength) {
+            return null;
+        }
+        
+        return _header.substring(pos);
+    }
+
+    public Token next() throws ParseException {
+        return readToken(NUL, false);
+    }
+    
+    /**
+     * Parses the next token from this String.
+     * If endOfAtom is not NUL, the token extends until the
+     * endOfAtom character is seen, or to the end of the header.
+     * This method is useful when parsing headers that don't
+     * obey the MIME specification, e.g., by failing to quote
+     * parameter values that contain spaces.
+     *
+     * @param   endOfAtom   if not NUL, character marking end of token
+     * @return      the next Token
+     * @exception   ParseException if the parse fails
+     * @since       JavaMail 1.5
+     */
+    public Token next(final char endOfAtom) throws ParseException {
+        return next(endOfAtom, false);
+    }
+
+    /**
+     * Parses the next token from this String.
+     * endOfAtom is handled as above.  If keepEscapes is true,
+     * any backslash escapes are preserved in the returned string.
+     * This method is useful when parsing headers that don't
+     * obey the MIME specification, e.g., by failing to escape
+     * backslashes in the filename parameter.
+     *
+     * @param   endOfAtom   if not NUL, character marking end of token
+     * @param   keepEscapes keep all backslashes in returned string?
+     * @return      the next Token
+     * @exception   ParseException if the parse fails
+     * @since       JavaMail 1.5
+     */
+    public Token next(final char endOfAtom, final boolean keepEscapes)
+                throws ParseException {        
+        return readToken(endOfAtom, keepEscapes);
+    }
+                
+
+    public Token peek() throws ParseException {
+        final int start = pos;
+        try {
+            return readToken(NUL, false);
+        } finally {
+            pos = start;
+        }
+    }
+
+    /**
+     * Read an ATOM token from the parsed header.
+     *
+     * @return A token containing the value of the atom token.
+     */
+    private Token readAtomicToken() {
+        // skip to next delimiter
+        final int start = pos;
+        final StringBuilder sb = new StringBuilder();
+        sb.append(_header.charAt(pos));
+        while (++pos < _headerLength) {
+            // break on the first non-atom character.
+            final char ch = _header.charAt(pos);
+         
+            if ((_delimiters.indexOf(_header.charAt(pos)) != -1 || ch < 32 || ch >= 127)) {
+                break;
+            }
+        }
+
+        return new Token(Token.ATOM, _header.substring(start, pos));
+    }
+
+    /**
+     * Read the next token from the header.
+     *
+     * @return The next token from the header.  White space is skipped, and comment
+     *         tokens are also skipped if indicated.
+     * @exception ParseException
+     */
+    private Token readToken(final char endOfAtom, final boolean keepEscapes) throws ParseException {
+        if (pos >= _headerLength) {
+            return EOF;
+        } else {
+            final char c = _header.charAt(pos);
+            // comment token...read and skip over this
+            if (c == '(') {
+                final Token comment = readComment(keepEscapes);
+                if (_skip) {
+                    return readToken(endOfAtom, keepEscapes);
+                } else {
+                    return comment;
+                }
+                // quoted literal
+            } else if (c == '\"') {
+                return readQuotedString('"', keepEscapes, 1);
+            // white space, eat this and find a real token.
+            } else if (WHITE.indexOf(c) != -1) {
+                eatWhiteSpace();
+                return readToken(endOfAtom, keepEscapes);
+            // either a CTL or special.  These characters have a self-defining token type.
+            } else if (c < 32 || c >= 127 || _delimiters.indexOf(c) != -1) {
+                
+                if (endOfAtom != NUL && c != endOfAtom) {
+                    return readQuotedString(endOfAtom, keepEscapes, 0);
+                }
+                
+                
+                pos++;
+                return new Token(c, String.valueOf(c));
+            } else {
+                // start of an atom, parse it off.
+                if (endOfAtom != NUL && c != endOfAtom) {
+                    return readQuotedString(endOfAtom, keepEscapes, 0);
+                }
+                
+                return readAtomicToken();
+            }
+        }
+    }
+
+    /**
+     * Extract a substring from the header string and apply any
+     * escaping/folding rules to the string.
+     *
+     * @param start  The starting offset in the header.
+     * @param end    The header end offset + 1.
+     *
+     * @return The processed string value.
+     * @exception ParseException
+     */
+    private String getEscapedValue(final int start, final int end, final boolean keepEscapes) throws ParseException {
+        final StringBuffer value = new StringBuffer();
+
+        for (int i = start; i < end; i++) {
+            final char ch = _header.charAt(i);
+            // is this an escape character?
+            if (ch == '\\') {
+                i++;
+                if (i == end) {
+                    throw new ParseException("Invalid escape character");
+                }
+                
+                if(keepEscapes) {
+                    value.append("\\");
+                }
+                
+                value.append(_header.charAt(i));
+            }
+            // line breaks are ignored, except for naked '\n' characters, which are consider
+            // parts of linear whitespace.
+            else if (ch == '\r') {
+                // see if this is a CRLF sequence, and skip the second if it is.
+                if (i < end - 1 && _header.charAt(i + 1) == '\n') {
+                    i++;
+                }
+            }
+            else {
+                 
+                 // just append the ch value.
+                value.append(ch);
+            }
+        }
+        return value.toString();
+    }
+
+    /**
+     * Read a comment from the header, applying nesting and escape
+     * rules to the content.
+     *
+     * @return A comment token with the token value.
+     * @exception ParseException
+     */
+    private Token readComment(final boolean keepEscapes) throws ParseException {
+        final int start = pos + 1;
+        int nesting = 1;
+
+        boolean requiresEscaping = false;
+
+        // skip to end of comment/string
+        while (++pos < _headerLength) {
+            final char ch = _header.charAt(pos);
+            if (ch == ')') {
+                nesting--;
+                if (nesting == 0) {
+                    break;
+                }
+            }
+            else if (ch == '(') {
+                nesting++;
+            }
+            else if (ch == '\\') {
+                pos++;
+                requiresEscaping = true;
+            }
+            // we need to process line breaks also
+            else if (ch == '\r') {
+                requiresEscaping = true;
+            }
+        }
+
+        if (nesting != 0) {
+            throw new ParseException("Unbalanced comments");
+        }
+
+        String value;
+        if (requiresEscaping) {
+            value = getEscapedValue(start, pos, keepEscapes);
+        }
+        else {
+            value = _header.substring(start, pos++);
+        }
+        return new Token(Token.COMMENT, value);
+    }
+
+    /**
+     * Parse out a quoted string from the header, applying escaping
+     * rules to the value.
+     *
+     * @return The QUOTEDSTRING token with the value.
+     * @exception ParseException
+     */
+    private Token readQuotedString(final char endChar, final boolean keepEscapes, final int offset) throws ParseException {
+        final int start = pos+offset;
+        boolean requiresEscaping = false;
+
+        // skip to end of comment/string
+        while (++pos < _headerLength) {
+            final char ch = _header.charAt(pos);
+
+            if (ch == endChar) {
+                String value;
+                if (requiresEscaping) {
+                    value = getEscapedValue(start, pos++, keepEscapes);
+                }
+                else {
+                    value = _header.substring(start, pos++);
+                }
+                return new Token(Token.QUOTEDSTRING, value);
+            }
+            else if (ch == '\\') {
+                pos++;
+                requiresEscaping = true;
+            }
+            // we need to process line breaks also
+            else if (ch == '\r') {
+                requiresEscaping = true;
+            }
+        }
+
+        throw new ParseException("Missing '\"'");
+    }
+
+    /**
+     * Skip white space in the token string.
+     */
+    private void eatWhiteSpace() {
+        // skip to end of whitespace
+        while (++pos < _headerLength
+                && WHITE.indexOf(_header.charAt(pos)) != -1) {
+			;
+		}
+    }
+}

Added: geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetAddress.java
URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetAddress.java?rev=1620683&view=auto
==============================================================================
--- geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetAddress.java (added)
+++ geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetAddress.java Tue Aug 26 18:17:06 2014
@@ -0,0 +1,576 @@
+/*
+ * 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 javax.mail.internet;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import javax.mail.Address;
+import javax.mail.Session;
+
+/**
+ * A representation of an Internet email address as specified by RFC822 in
+ * conjunction with a human-readable personal name that can be encoded as
+ * specified by RFC2047.
+ * A typical address is "user@host.domain" and personal name "Joe User"
+ *
+ * @version $Rev$ $Date$
+ */
+public class InternetAddress extends Address implements Cloneable {
+	
+	private static final long serialVersionUID = -7507595530758302903L;
+	
+    /**
+     * The address in RFC822 format.
+     */
+    protected String address;
+
+    /**
+     * The personal name in RFC2047 format.
+     * Subclasses must ensure that this field is updated if the personal field
+     * is updated; alternatively, it can be invalidated by setting to null
+     * which will cause it to be recomputed.
+     */
+    protected String encodedPersonal;
+
+    /**
+     * The personal name as a Java String.
+     * Subclasses must ensure that this field is updated if the encodedPersonal field
+     * is updated; alternatively, it can be invalidated by setting to null
+     * which will cause it to be recomputed.
+     */
+    protected String personal;
+
+    public InternetAddress() {
+    }
+
+    public InternetAddress(final String address) throws AddressException {
+        this(address, true);
+    }
+
+    public InternetAddress(final String address, final boolean strict) throws AddressException {
+        // use the parse method to process the address.  This has the wierd side effect of creating a new
+        // InternetAddress instance to create an InternetAddress, but these are lightweight objects and
+        // we need access to multiple pieces of data from the parsing process.
+        final AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
+
+        final InternetAddress parsedAddress = parser.parseAddress();
+        // copy the important information, which right now is just the address and
+        // personal info.
+        this.address = parsedAddress.address;
+        this.personal = parsedAddress.personal;
+        this.encodedPersonal = parsedAddress.encodedPersonal;
+    }
+
+    public InternetAddress(final String address, final String personal) throws UnsupportedEncodingException {
+        this(address, personal, null);
+    }
+
+    public InternetAddress(final String address, final String personal, final String charset) throws UnsupportedEncodingException {
+        this.address = address;
+        setPersonal(personal, charset);
+    }
+
+    /**
+     * Clone this object.
+     *
+     * @return a copy of this object as created by Object.clone()
+     */
+    @Override
+    public Object clone() {
+        try {
+            return super.clone();
+        } catch (final CloneNotSupportedException e) {
+            throw new Error();
+        }
+    }
+
+    /**
+     * Return the type of this address.
+     *
+     * @return the type of this address; always "rfc822"
+     */
+    @Override
+    public String getType() {
+        return "rfc822";
+    }
+
+    /**
+     * Set the address.
+     * No validation is performed; validate() can be used to check if it is valid.
+     *
+     * @param address the address to set
+     */
+    public void setAddress(final String address) {
+        this.address = address;
+    }
+
+    /**
+     * Set the personal name.
+     * The name is first checked to see if it can be encoded; if this fails then an
+     * UnsupportedEncodingException is thrown and no fields are modified.
+     *
+     * @param name    the new personal name
+     * @param charset the charset to use; see {@link MimeUtility#encodeWord(String, String, String) MimeUtilityencodeWord}
+     * @throws UnsupportedEncodingException if the name cannot be encoded
+     */
+    public void setPersonal(final String name, final String charset) throws UnsupportedEncodingException {
+        personal = name;
+        if (name != null) {
+            encodedPersonal = MimeUtility.encodeWord(name, charset, null);
+        }
+        else {
+            encodedPersonal = null;
+        }
+    }
+
+    /**
+     * Set the personal name.
+     * The name is first checked to see if it can be encoded using {@link MimeUtility#encodeWord(String)}; if this fails then an
+     * UnsupportedEncodingException is thrown and no fields are modified.
+     *
+     * @param name the new personal name
+     * @throws UnsupportedEncodingException if the name cannot be encoded
+     */
+    public void setPersonal(final String name) throws UnsupportedEncodingException {
+        personal = name;
+        if (name != null) {
+            encodedPersonal = MimeUtility.encodeWord(name);
+        }
+        else {
+            encodedPersonal = null;
+        }
+    }
+
+    /**
+     * Return the address.
+     *
+     * @return the address
+     */
+    public String getAddress() {
+        return address;
+    }
+
+    /**
+     * Return the personal name.
+     * If the personal field is null, then an attempt is made to decode the encodedPersonal
+     * field using {@link MimeUtility#decodeWord(String)}; if this is sucessful, then
+     * the personal field is updated with that value and returned; if there is a problem
+     * decoding the text then the raw value from encodedPersonal is returned.
+     *
+     * @return the personal name
+     */
+    public String getPersonal() {
+        if (personal == null && encodedPersonal != null) {
+            try {
+                personal = MimeUtility.decodeWord(encodedPersonal);
+            } catch (final ParseException e) {
+                return encodedPersonal;
+            } catch (final UnsupportedEncodingException e) {
+                return encodedPersonal;
+            }
+        }
+        return personal;
+    }
+
+    /**
+     * Return the encoded form of the personal name.
+     * If the encodedPersonal field is null, then an attempt is made to encode the
+     * personal field using {@link MimeUtility#encodeWord(String)}; if this is
+     * successful then the encodedPersonal field is updated with that value and returned;
+     * if there is a problem encoding the text then null is returned.
+     *
+     * @return the encoded form of the personal name
+     */
+    private String getEncodedPersonal() {
+        if (encodedPersonal == null && personal != null) {
+            try {
+                encodedPersonal = MimeUtility.encodeWord(personal);
+            } catch (final UnsupportedEncodingException e) {
+                // as we could not encode this, return null
+                return null;
+            }
+        }
+        return encodedPersonal;
+    }
+
+
+    /**
+     * Return a string representation of this address using only US-ASCII characters.
+     *
+     * @return a string representation of this address
+     */
+    @Override
+    public String toString() {
+        // group addresses are always returned without modification.
+        if (isGroup()) {
+            return address;
+        }
+
+        // if we have personal information, then we need to return this in the route-addr form:
+        // "personal <address>".  If there is no personal information, then we typically return
+        // the address without the angle brackets.  However, if the address contains anything other
+        // than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses
+        // quoted strings in the local-part), we bracket the address.
+        final String p = getEncodedPersonal();
+        if (p == null) {
+            return formatAddress(address);
+        }
+        else {
+            final StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3);
+            buf.append(AddressParser.quoteString(p));
+            buf.append(" <").append(address).append(">");
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Check the form of an address, and enclose it within brackets
+     * if they are required for this address form.
+     *
+     * @param a      The source address.
+     *
+     * @return A formatted address, which can be the original address string.
+     */
+    private String formatAddress(final String a)
+    {
+        // this could be a group address....we don't muck with those.
+        if (address.endsWith(";") && address.indexOf(":") > 0) {
+            return address;
+        }
+
+        if (AddressParser.containsCharacters(a, "()<>,;:\"[]")) {
+            final StringBuffer buf = new StringBuffer(address.length() + 3);
+            buf.append("<").append(address).append(">");
+            return buf.toString();
+        }
+        return address;
+    }
+
+    /**
+     * Return a string representation of this address using Unicode characters.
+     *
+     * @return a string representation of this address
+     */
+    public String toUnicodeString() {
+        // group addresses are always returned without modification.
+        if (isGroup()) {
+            return address;
+        }
+
+        // if we have personal information, then we need to return this in the route-addr form:
+        // "personal <address>".  If there is no personal information, then we typically return
+        // the address without the angle brackets.  However, if the address contains anything other
+        // than atoms, '@', and '.' (e.g., uses domain literals, has specified routes, or uses
+        // quoted strings in the local-part), we bracket the address.
+
+        // NB:  The difference between toString() and toUnicodeString() is the use of getPersonal()
+        // vs. getEncodedPersonal() for the personal portion.  If the personal information contains only
+        // ASCII-7 characters, these are the same.
+        final String p = getPersonal();
+        if (p == null) {
+            return formatAddress(address);
+        }
+        else {
+            final StringBuffer buf = new StringBuffer(p.length() + 8 + address.length() + 3);
+            buf.append(AddressParser.quoteString(p));
+            buf.append(" <").append(address).append(">");
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Compares two addresses for equality.
+     * We define this as true if the other object is an InternetAddress
+     * and the two values returned by getAddress() are equal in a
+     * case-insensitive comparison.
+     *
+     * @param o the other object
+     * @return true if the addresses are the same
+     */
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+			return true;
+		}
+        if (!(o instanceof InternetAddress)) {
+			return false;
+		}
+
+        final InternetAddress other = (InternetAddress) o;
+        final String myAddress = getAddress();
+        return myAddress == null ? (other.getAddress() == null) : myAddress.equalsIgnoreCase(other.getAddress());
+    }
+
+    /**
+     * Return the hashCode for this address.
+     * We define this to be the hashCode of the address after conversion to lowercase.
+     *
+     * @return a hashCode for this address
+     */
+    @Override
+    public int hashCode() {
+        return (address == null) ? 0 : address.toLowerCase().hashCode();
+    }
+
+    /**
+     * Return true is this address is an RFC822 group address in the format
+     * <code>phrase ":" [#mailbox] ";"</code>.
+     * We check this by using the presense of a ':' character in the address, and a
+     * ';' as the very last character.
+     *
+     * @return true is this address represents a group
+     */
+    public boolean isGroup() {
+        if (address == null) {
+            return false;
+        }
+
+        return address.endsWith(";") && address.indexOf(":") > 0;
+    }
+
+    /**
+     * Return the members of a group address.
+     *
+     * If strict is true and the address does not contain an initial phrase then an AddressException is thrown.
+     * Otherwise the phrase is skipped and the remainder of the address is checked to see if it is a group.
+     * If it is, the content and strict flag are passed to parseHeader to extract the list of addresses;
+     * if it is not a group then null is returned.
+     *
+     * @param strict whether strict RFC822 checking should be performed
+     * @return an array of InternetAddress objects for the group members, or null if this address is not a group
+     * @throws AddressException if there was a problem parsing the header
+     */
+    public InternetAddress[] getGroup(final boolean strict) throws AddressException {
+        if (address == null) {
+            return null;
+        }
+
+        // create an address parser and use it to extract the group information.
+        final AddressParser parser = new AddressParser(address, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
+        return parser.extractGroupList();
+    }
+
+    /**
+     * Return an InternetAddress representing the current user.
+     * <P/>
+     * If session is not null, we first look for an address specified in its
+     * "mail.from" property; if this is not set, we look at its "mail.user"
+     * and "mail.host" properties and if both are not null then an address of
+     * the form "${mail.user}@${mail.host}" is created.
+     * If this fails to give an address, then an attempt is made to create
+     * an address by combining the value of the "user.name" System property
+     * with the value returned from InetAddress.getLocalHost().getHostName().
+     * Any SecurityException raised accessing the system property or any
+     * UnknownHostException raised getting the hostname are ignored.
+     * <P/>
+     * Finally, an attempt is made to convert the value obtained above to
+     * an InternetAddress. If this fails, then null is returned.
+     *
+     * @param session used to obtain mail properties
+     * @return an InternetAddress for the current user, or null if it cannot be determined
+     */
+    public static InternetAddress getLocalAddress(final Session session) {
+        String host = null;
+        String user = null;
+
+        // ok, we have several steps for resolving this.  To start with, we could have a from address
+        // configured already, which will be a full InternetAddress string.  If we don't have that, then
+        // we need to resolve a user and host to compose an address from.
+        if (session != null) {
+            final String address = session.getProperty("mail.from");
+            // if we got this, we can skip out now
+            if (address != null) {
+                try {
+                    return new InternetAddress(address);
+                } catch (final AddressException e) {
+                    // invalid address on the from...treat this as an error and return null.
+                    return null;
+                }
+            }
+
+            // now try for user and host information.  We have both session and system properties to check here.
+            // we'll just handle the session ones here, and check the system ones below if we're missing information.
+            user = session.getProperty("mail.user");
+            host = session.getProperty("mail.host");
+        }
+
+        try {
+
+            // if either user or host is null, then we check non-session sources for the information.
+            if (user == null) {
+                user = System.getProperty("user.name");
+            }
+
+            if (host == null) {
+                host = InetAddress.getLocalHost().getHostName();
+            }
+
+            if (user != null && host != null) {
+                // if we have both a user and host, we can create a local address
+                return new InternetAddress(user + '@' + host);
+            }
+        } catch (final AddressException e) {
+            // ignore
+        } catch (final UnknownHostException e) {
+            // ignore
+        } catch (final SecurityException e) {
+            // ignore
+        }
+        return null;
+    }
+
+    /**
+     * Convert the supplied addresses into a single String of comma-separated text as
+     * produced by {@link InternetAddress#toString() toString()}.
+     * No line-break detection is performed.
+     *
+     * @param addresses the array of addresses to convert
+     * @return a one-line String of comma-separated addresses
+     */
+    public static String toString(final Address[] addresses) {
+        if (addresses == null || addresses.length == 0) {
+            return null;
+        }
+        if (addresses.length == 1) {
+            return addresses[0].toString();
+        } else {
+            final StringBuffer buf = new StringBuffer(addresses.length * 32);
+            buf.append(addresses[0].toString());
+            for (int i = 1; i < addresses.length; i++) {
+                buf.append(", ");
+                buf.append(addresses[i].toString());
+            }
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Convert the supplies addresses into a String of comma-separated text,
+     * inserting line-breaks between addresses as needed to restrict the line
+     * length to 72 characters. Splits will only be introduced between addresses
+     * so an address longer than 71 characters will still be placed on a single
+     * line.
+     *
+     * @param addresses the array of addresses to convert
+     * @param used      the starting column
+     * @return a String of comma-separated addresses with optional line breaks
+     */
+    public static String toString(final Address[] addresses, int used) {
+        if (addresses == null || addresses.length == 0) {
+            return null;
+        }
+        if (addresses.length == 1) {
+            String s = addresses[0].toString();
+            if (used + s.length() > 72) {
+                s = "\r\n  " + s;
+            }
+            return s;
+        } else {
+            final StringBuffer buf = new StringBuffer(addresses.length * 32);
+            for (int i = 0; i < addresses.length; i++) {
+                final String s = addresses[1].toString();
+                if (i == 0) {
+                    if (used + s.length() + 1 > 72) {
+                        buf.append("\r\n  ");
+                        used = 2;
+                    }
+                } else {
+                    if (used + s.length() + 1 > 72) {
+                        buf.append(",\r\n  ");
+                        used = 2;
+                    } else {
+                        buf.append(", ");
+                        used += 2;
+                    }
+                }
+                buf.append(s);
+                used += s.length();
+            }
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Parse addresses out of the string with basic checking.
+     *
+     * @param addresses the addresses to parse
+     * @return an array of InternetAddresses parsed from the string
+     * @throws AddressException if addresses checking fails
+     */
+    public static InternetAddress[] parse(final String addresses) throws AddressException {
+        return parse(addresses, true);
+    }
+
+    /**
+     * Parse addresses out of the string.
+     *
+     * @param addresses the addresses to parse
+     * @param strict if true perform detailed checking, if false just perform basic checking
+     * @return an array of InternetAddresses parsed from the string
+     * @throws AddressException if address checking fails
+     */
+    public static InternetAddress[] parse(final String addresses, final boolean strict) throws AddressException {
+        return parse(addresses, strict ? AddressParser.STRICT : AddressParser.NONSTRICT);
+    }
+
+    /**
+     * Parse addresses out of the string.
+     *
+     * @param addresses the addresses to parse
+     * @param strict if true perform detailed checking, if false perform little checking
+     * @return an array of InternetAddresses parsed from the string
+     * @throws AddressException if address checking fails
+     */
+    public static InternetAddress[] parseHeader(final String addresses, final boolean strict) throws AddressException {
+        return parse(addresses, strict ? AddressParser.STRICT : AddressParser.PARSE_HEADER);
+    }
+
+    /**
+     * Parse addresses with increasing degrees of RFC822 compliance checking.
+     *
+     * @param addresses the string to parse
+     * @param level     The required strictness level.
+     *
+     * @return an array of InternetAddresses parsed from the string
+     * @throws AddressException
+     *                if address checking fails
+     */
+    private static InternetAddress[] parse(final String addresses, final int level) throws AddressException {
+        // create a parser and have it extract the list using the requested strictness leve.
+        final AddressParser parser = new AddressParser(addresses, level);
+        return parser.parseAddressList();
+    }
+
+    /**
+     * Validate the address portion of an internet address to ensure
+     * validity.   Throws an AddressException if any validity
+     * problems are encountered.
+     *
+     * @exception AddressException
+     */
+    public void validate() throws AddressException {
+
+        // create a parser using the strictest validation level.
+        final AddressParser parser = new AddressParser(formatAddress(address), AddressParser.STRICT);
+        parser.validateAddress();
+    }
+}

Added: geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetHeaders.java
URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetHeaders.java?rev=1620683&view=auto
==============================================================================
--- geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetHeaders.java (added)
+++ geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/InternetHeaders.java Tue Aug 26 18:17:06 2014
@@ -0,0 +1,716 @@
+/*
+ * 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 javax.mail.internet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+import javax.mail.Address;
+import javax.mail.Header;
+import javax.mail.MessagingException;
+
+/**
+ * Class that represents the RFC822 headers associated with a message.
+ *
+ * @version $Rev$ $Date$
+ */
+public class InternetHeaders {
+    // the list of headers (to preserve order);
+    protected List headers = new ArrayList();
+
+    /**
+     * Create an empty InternetHeaders
+     */
+    public InternetHeaders() {
+        // these are created in the preferred order of the headers.
+        addHeader("Return-Path", null);
+        addHeader("Received", null);
+        addHeader("Resent-Date", null);
+        addHeader("Resent-From", null);
+        addHeader("Resent-Sender", null);
+        addHeader("Resent-To", null);
+        addHeader("Resent-Cc", null);
+        addHeader("Resent-Bcc", null);
+        addHeader("Resent-Message-Id", null);
+        addHeader("Date", null);
+        addHeader("From", null);
+        addHeader("Sender", null);
+        addHeader("Reply-To", null);
+        addHeader("To", null);
+        addHeader("Cc", null);
+        addHeader("Bcc", null);
+        addHeader("Message-Id", null);
+        addHeader("In-Reply-To", null);
+        addHeader("References", null);
+        addHeader("Subject", null);
+        addHeader("Comments", null);
+        addHeader("Keywords", null);
+        addHeader("Errors-To", null);
+        addHeader("MIME-Version", null);
+        addHeader("Content-Type", null);
+        addHeader("Content-Transfer-Encoding", null);
+        addHeader("Content-MD5", null);
+        // the following is a special marker used to identify new header insertion points.
+        addHeader(":", null);
+        addHeader("Content-Length", null);
+        addHeader("Status", null);
+    }
+
+    /**
+     * Create a new InternetHeaders initialized by reading headers from the
+     * stream.
+     *
+     * @param in
+     *            the RFC822 input stream to load from
+     * @throws MessagingException
+     *             if there is a problem pasring the stream
+     */
+    public InternetHeaders(final InputStream in) throws MessagingException {
+        load(in);
+    }
+
+    /**
+     * Read and parse the supplied stream and add all headers to the current
+     * set.
+     *
+     * @param in
+     *            the RFC822 input stream to load from
+     * @throws MessagingException
+     *             if there is a problem pasring the stream
+     */
+    public void load(final InputStream in) throws MessagingException {
+        try {
+            final StringBuffer buffer = new StringBuffer(128);
+            String line;
+            // loop until we hit the end or a null line
+            while ((line = readLine(in)) != null) {
+                // lines beginning with white space get special handling
+                if (line.startsWith(" ") || line.startsWith("\t")) {
+                    // this gets handled using the logic defined by
+                    // the addHeaderLine method.  If this line is a continuation, but
+                    // there's nothing before it, just call addHeaderLine to add it
+                    // to the last header in the headers list
+                    if (buffer.length() == 0) {
+                        addHeaderLine(line);
+                    }
+                    else {
+                        // preserve the line break and append the continuation
+                        buffer.append("\r\n");
+                        buffer.append(line);
+                    }
+                }
+                else {
+                    // if we have a line pending in the buffer, flush it
+                    if (buffer.length() > 0) {
+                        addHeaderLine(buffer.toString());
+                        buffer.setLength(0);
+                    }
+                    // add this to the accumulator
+                    buffer.append(line);
+                }
+            }
+
+            // if we have a line pending in the buffer, flush it
+            if (buffer.length() > 0) {
+                addHeaderLine(buffer.toString());
+            }
+        } catch (final IOException e) {
+            throw new MessagingException("Error loading headers", e);
+        }
+    }
+
+
+    /**
+     * Read a single line from the input stream
+     *
+     * @param in     The source stream for the line
+     *
+     * @return The string value of the line (without line separators)
+     */
+    private String readLine(final InputStream in) throws IOException {
+        final StringBuffer buffer = new StringBuffer(128);
+
+        int c;
+
+        while ((c = in.read()) != -1) {
+            // a linefeed is a terminator, always.
+            if (c == '\n') {
+                break;
+            }
+            // just ignore the CR.  The next character SHOULD be an NL.  If not, we're
+            // just going to discard this
+            else if (c == '\r') {
+                continue;
+            }
+            else {
+                // just add to the buffer
+                buffer.append((char)c);
+            }
+        }
+
+        // no characters found...this was either an eof or a null line.
+        if (buffer.length() == 0) {
+            return null;
+        }
+
+        return buffer.toString();
+    }
+
+
+    /**
+     * Return all the values for the specified header.
+     *
+     * @param name
+     *            the header to return
+     * @return the values for that header, or null if the header is not present
+     */
+    public String[] getHeader(final String name) {
+        final List accumulator = new ArrayList();
+
+        for (int i = 0; i < headers.size(); i++) {
+            final InternetHeader header = (InternetHeader)headers.get(i);
+            if (header.getName().equalsIgnoreCase(name) && header.getValue() != null) {
+                accumulator.add(header.getValue());
+            }
+        }
+
+        // this is defined as returning null of nothing is found.
+        if (accumulator.isEmpty()) {
+            return null;
+        }
+
+        // convert this to an array.
+        return (String[])accumulator.toArray(new String[accumulator.size()]);
+    }
+
+    /**
+     * Return the values for the specified header as a single String. If the
+     * header has more than one value then all values are concatenated together
+     * separated by the supplied delimiter.
+     *
+     * @param name
+     *            the header to return
+     * @param delimiter
+     *            the delimiter used in concatenation
+     * @return the header as a single String
+     */
+    public String getHeader(final String name, final String delimiter) {
+        // get all of the headers with this name
+        final String[] matches = getHeader(name);
+
+        // no match?  return a null.
+        if (matches == null) {
+            return null;
+        }
+
+        // a null delimiter means just return the first one.  If there's only one item, this is easy too.
+        if (matches.length == 1 || delimiter == null) {
+            return matches[0];
+        }
+
+        // perform the concatenation
+        final StringBuffer result = new StringBuffer(matches[0]);
+
+        for (int i = 1; i < matches.length; i++) {
+            result.append(delimiter);
+            result.append(matches[i]);
+        }
+
+        return result.toString();
+    }
+
+
+    /**
+     * Set the value of the header to the supplied value; any existing headers
+     * are removed.
+     *
+     * @param name
+     *            the name of the header
+     * @param value
+     *            the new value
+     */
+    public void setHeader(final String name, final String value) {
+        // look for a header match
+        for (int i = 0; i < headers.size(); i++) {
+            final InternetHeader header = (InternetHeader)headers.get(i);
+            // found a matching header
+            if (name.equalsIgnoreCase(header.getName())) {
+                // we update both the name and the value for a set so that
+                // the header ends up with the same case as what is getting set
+                header.setValue(value);
+                header.setName(name);
+                // remove all of the headers from this point
+                removeHeaders(name, i + 1);
+                return;
+            }
+        }
+
+        // doesn't exist, so process as an add.
+        addHeader(name, value);
+    }
+
+
+    /**
+     * Remove all headers with the given name, starting with the
+     * specified start position.
+     *
+     * @param name   The target header name.
+     * @param pos    The position of the first header to examine.
+     */
+    private void removeHeaders(final String name, final int pos) {
+        // now go remove all other instances of this header
+        for (int i = pos; i < headers.size(); i++) {
+            final InternetHeader header = (InternetHeader)headers.get(i);
+            // found a matching header
+            if (name.equalsIgnoreCase(header.getName())) {
+                // remove this item, and back up
+                headers.remove(i);
+                i--;
+            }
+        }
+    }
+
+
+    /**
+     * Find a header in the current list by name, returning the index.
+     *
+     * @param name   The target name.
+     *
+     * @return The index of the header in the list.  Returns -1 for a not found
+     *         condition.
+     */
+    private int findHeader(final String name) {
+        return findHeader(name, 0);
+    }
+
+
+    /**
+     * Find a header in the current list, beginning with the specified
+     * start index.
+     *
+     * @param name   The target header name.
+     * @param start  The search start index.
+     *
+     * @return The index of the first matching header.  Returns -1 if the
+     *         header is not located.
+     */
+    private int findHeader(final String name, final int start) {
+        for (int i = start; i < headers.size(); i++) {
+            final InternetHeader header = (InternetHeader)headers.get(i);
+            // found a matching header
+            if (name.equalsIgnoreCase(header.getName())) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Add a new value to the header with the supplied name.
+     *
+     * @param name
+     *            the name of the header to add a new value for
+     * @param value
+     *            another value
+     */
+    public void addHeader(final String name, final String value) {
+        final InternetHeader newHeader = new InternetHeader(name, value);
+
+        // The javamail spec states that "Recieved" headers need to be added in reverse order.
+        // Return-Path is permitted before Received, so handle it the same way.
+        if (name.equalsIgnoreCase("Received") || name.equalsIgnoreCase("Return-Path")) {
+            // see if we have one of these already
+            final int pos = findHeader(name);
+
+            // either insert before an existing header, or insert at the very beginning
+            if (pos != -1) {
+                // this could be a placeholder header with a null value.  If it is, just update
+                // the value.  Otherwise, insert in front of the existing header.
+                final InternetHeader oldHeader = (InternetHeader)headers.get(pos);
+                if (oldHeader.getValue() == null) {
+                    oldHeader.setValue(value);
+                }
+                else {
+                    headers.add(pos, newHeader);
+                }
+            }
+            else {
+                // doesn't exist, so insert at the beginning
+                headers.add(0, newHeader);
+            }
+        }
+        // normal insertion
+        else {
+            // see if we have one of these already
+            int pos = findHeader(name);
+
+            // either insert before an existing header, or insert at the very beginning
+            if (pos != -1) {
+                final InternetHeader oldHeader = (InternetHeader)headers.get(pos);
+                // if the existing header is a place holder, we can just update the value
+                if (oldHeader.getValue() == null) {
+                    oldHeader.setValue(value);
+                }
+                else {
+                    // we have at least one existing header with this name.  We need to find the last occurrance,
+                    // and insert after that spot.
+
+                    int lastPos = findHeader(name, pos + 1);
+
+                    while (lastPos != -1) {
+                        pos = lastPos;
+                        lastPos = findHeader(name, pos + 1);
+                    }
+
+                    // ok, we have the insertion position
+                    headers.add(pos + 1, newHeader);
+                }
+            }
+            else {
+                // find the insertion marker.  If that is missing somehow, insert at the end.
+                pos = findHeader(":");
+                if (pos == -1) {
+                    pos = headers.size();
+                }
+                headers.add(pos, newHeader);
+            }
+        }
+    }
+
+
+    /**
+     * Remove all header entries with the supplied name
+     *
+     * @param name
+     *            the header to remove
+     */
+    public void removeHeader(final String name) {
+        // the first occurrance of a header is just zeroed out.
+        final int pos = findHeader(name);
+
+        if (pos != -1) {
+            final InternetHeader oldHeader = (InternetHeader)headers.get(pos);
+            // keep the header in the list, but with a null value
+            oldHeader.setValue(null);
+            // now remove all other headers with this name
+            removeHeaders(name, pos + 1);
+        }
+    }
+
+
+    /**
+     * Return all headers.
+     *
+     * @return an Enumeration<Header> containing all headers
+     */
+    public Enumeration getAllHeaders() {
+        final List result = new ArrayList();
+
+        for (int i = 0; i < headers.size(); i++) {
+            final InternetHeader header = (InternetHeader)headers.get(i);
+            // we only include headers with real values, no placeholders
+            if (header.getValue() != null) {
+                result.add(header);
+            }
+        }
+        // just return a list enumerator for the header list.
+        return Collections.enumeration(result);
+    }
+
+
+    /**
+     * Test if a given header name is a match for any header in the
+     * given list.
+     *
+     * @param name   The name of the current tested header.
+     * @param names  The list of names to match against.
+     *
+     * @return True if this is a match for any name in the list, false
+     *         for a complete mismatch.
+     */
+    private boolean matchHeader(final String name, final String[] names) {
+        // the list of names is not required, so treat this as if it
+        // was an empty list and we didn't get a match.
+        if (names == null) {
+            return false;
+        }
+
+        for (int i = 0; i < names.length; i++) {
+            if (name.equalsIgnoreCase(names[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    /**
+     * Return all matching Header objects.
+     */
+    public Enumeration getMatchingHeaders(final String[] names) {
+        final List result = new ArrayList();
+
+        for (int i = 0; i < headers.size(); i++) {
+            final InternetHeader header = (InternetHeader)headers.get(i);
+            // we only include headers with real values, no placeholders
+            if (header.getValue() != null) {
+                // only add the matching ones
+                if (matchHeader(header.getName(), names)) {
+                    result.add(header);
+                }
+            }
+        }
+        return Collections.enumeration(result);
+    }
+
+
+    /**
+     * Return all non matching Header objects.
+     */
+    public Enumeration getNonMatchingHeaders(final String[] names) {
+        final List result = new ArrayList();
+
+        for (int i = 0; i < headers.size(); i++) {
+            final InternetHeader header = (InternetHeader)headers.get(i);
+            // we only include headers with real values, no placeholders
+            if (header.getValue() != null) {
+                // only add the non-matching ones
+                if (!matchHeader(header.getName(), names)) {
+                    result.add(header);
+                }
+            }
+        }
+        return Collections.enumeration(result);
+    }
+
+
+    /**
+     * Add an RFC822 header line to the header store. If the line starts with a
+     * space or tab (a continuation line), add it to the last header line in the
+     * list. Otherwise, append the new header line to the list.
+     *
+     * Note that RFC822 headers can only contain US-ASCII characters
+     *
+     * @param line
+     *            raw RFC822 header line
+     */
+    public void addHeaderLine(final String line) {
+        // null lines are a nop
+        if (line.length() == 0) {
+            return;
+        }
+
+        // we need to test the first character to see if this is a continuation whitespace
+        final char ch = line.charAt(0);
+
+        // tabs and spaces are special.  This is a continuation of the last header in the list.
+        if (ch == ' ' || ch == '\t') {
+            final int size = headers.size();
+            // it's possible that we have a leading blank line.
+            if (size > 0) {
+                final InternetHeader header = (InternetHeader)headers.get(size - 1);
+                header.appendValue(line);
+            }
+        }
+        else {
+            // this just gets appended to the end, preserving the addition order.
+            headers.add(new InternetHeader(line));
+        }
+    }
+
+
+    /**
+     * Return all the header lines as an Enumeration of Strings.
+     */
+    public Enumeration getAllHeaderLines() {
+        return new HeaderLineEnumeration(getAllHeaders());
+    }
+
+    /**
+     * Return all matching header lines as an Enumeration of Strings.
+     */
+    public Enumeration getMatchingHeaderLines(final String[] names) {
+        return new HeaderLineEnumeration(getMatchingHeaders(names));
+    }
+
+    /**
+     * Return all non-matching header lines.
+     */
+    public Enumeration getNonMatchingHeaderLines(final String[] names) {
+        return new HeaderLineEnumeration(getNonMatchingHeaders(names));
+    }
+
+
+    /**
+     * Set an internet header from a list of addresses.  The
+     * first address item is set, followed by a series of addHeaders().
+     *
+     * @param name      The name to set.
+     * @param addresses The list of addresses to set.
+     */
+    void setHeader(final String name, final Address[] addresses) {
+        // if this is empty, then we need to replace this
+        if (addresses.length == 0) {
+            removeHeader(name);
+        } else {
+
+            // replace the first header
+            setHeader(name, addresses[0].toString());
+
+            // now add the rest as extra headers.
+            for (int i = 1; i < addresses.length; i++) {
+                final Address address = addresses[i];
+                addHeader(name, address.toString());
+            }
+        }
+    }
+
+
+    /**
+     * Write out the set of headers, except for any
+     * headers specified in the optional ignore list.
+     *
+     * @param out    The output stream.
+     * @param ignore The optional ignore list.
+     *
+     * @exception IOException
+     */
+    void writeTo(final OutputStream out, final String[] ignore) throws IOException {
+        if (ignore == null) {
+            // write out all header lines with non-null values
+            for (int i = 0; i < headers.size(); i++) {
+                final InternetHeader header = (InternetHeader)headers.get(i);
+                // we only include headers with real values, no placeholders
+                if (header.getValue() != null) {
+                    header.writeTo(out);
+                }
+            }
+        }
+        else {
+            // write out all matching header lines with non-null values
+            for (int i = 0; i < headers.size(); i++) {
+                final InternetHeader header = (InternetHeader)headers.get(i);
+                // we only include headers with real values, no placeholders
+                if (header.getValue() != null) {
+                    if (!matchHeader(header.getName(), ignore)) {
+                        header.writeTo(out);
+                    }
+                }
+            }
+        }
+    }
+
+    protected static final class InternetHeader extends Header {
+
+        public InternetHeader(final String h) {
+            // initialize with null values, which we'll update once we parse the string
+            super("", "");
+            int separator = h.indexOf(':');
+            // no separator, then we take this as a name with a null string value.
+            if (separator == -1) {
+                name = h.trim();
+            }
+            else {
+                name = h.substring(0, separator);
+                // step past the separator.  Now we need to remove any leading white space characters.
+                separator++;
+
+                while (separator < h.length()) {
+                    final char ch = h.charAt(separator);
+                    if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
+                        break;
+                    }
+                    separator++;
+                }
+
+                value = h.substring(separator);
+            }
+        }
+
+        public InternetHeader(final String name, final String value) {
+            super(name, value);
+        }
+
+
+        /**
+         * Package scope method for setting the header value.
+         *
+         * @param value  The new header value.
+         */
+        void setValue(final String value) {
+            this.value = value;
+        }
+
+
+        /**
+         * Package scope method for setting the name value.
+         *
+         * @param name   The new header name
+         */
+        void setName(final String name) {
+            this.name = name;
+        }
+
+        /**
+         * Package scope method for extending a header value.
+         *
+         * @param value  The appended header value.
+         */
+        void appendValue(final String value) {
+            if (this.value == null) {
+                this.value = value;
+            }
+            else {
+                this.value = this.value + "\r\n" + value;
+            }
+        }
+
+        void writeTo(final OutputStream out) throws IOException {
+            out.write(name.getBytes("ISO8859-1"));
+            out.write(':');
+            out.write(' ');
+            out.write(value.getBytes("ISO8859-1"));
+            out.write('\r');
+            out.write('\n');
+        }
+    }
+
+    private static class HeaderLineEnumeration implements Enumeration {
+        private final Enumeration headers;
+
+        public HeaderLineEnumeration(final Enumeration headers) {
+            this.headers = headers;
+        }
+
+        public boolean hasMoreElements() {
+            return headers.hasMoreElements();
+        }
+
+        public Object nextElement() {
+            final Header h = (Header) headers.nextElement();
+            return h.getName() + ": " + h.getValue();
+        }
+    }
+}

Added: geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/MailDateFormat.java
URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/MailDateFormat.java?rev=1620683&view=auto
==============================================================================
--- geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/MailDateFormat.java (added)
+++ geronimo/specs/trunk/geronimo-javamail_1.5_spec/src/main/java/javax/mail/internet/MailDateFormat.java Tue Aug 26 18:17:06 2014
@@ -0,0 +1,618 @@
+/*
+ * 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 javax.mail.internet;
+
+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();
+    }
+    
+    
+    // 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; 
+            }
+        }
+    }
+}