001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package javax.mail.internet;
021    
022    import java.io.ByteArrayOutputStream;
023    import java.io.UnsupportedEncodingException;
024    import java.util.ArrayList;// Represents lists in things like
025    import java.util.Collections;
026    import java.util.Enumeration;
027    import java.util.HashMap;
028    import java.util.Iterator;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.StringTokenizer;
032    
033    import org.apache.geronimo.mail.util.ASCIIUtil;
034    import org.apache.geronimo.mail.util.RFC2231Encoder;
035    import org.apache.geronimo.mail.util.SessionUtil;
036    
037    // Content-Type: text/plain;charset=klingon
038    //
039    // The ;charset=klingon is the parameter list, may have more of them with ';'
040    
041    /**
042     * @version $Rev: 669445 $ $Date: 2008-06-19 06:48:18 -0400 (Thu, 19 Jun 2008) $
043     */
044    public class ParameterList {
045        private static final String MIME_ENCODEPARAMETERS = "mail.mime.encodeparameters";
046        private static final String MIME_DECODEPARAMETERS = "mail.mime.decodeparameters";
047        private static final String MIME_DECODEPARAMETERS_STRICT = "mail.mime.decodeparameters.strict";
048    
049        private static final int HEADER_SIZE_LIMIT = 76;
050    
051        private Map _parameters = new HashMap();
052    
053        private boolean encodeParameters = false;
054        private boolean decodeParameters = false;
055        private boolean decodeParametersStrict = false;
056    
057        public ParameterList() {
058            // figure out how parameter handling is to be performed.
059            getInitialProperties();
060        }
061    
062        public ParameterList(String list) throws ParseException {
063            // figure out how parameter handling is to be performed.
064            getInitialProperties();
065            // get a token parser for the type information
066            HeaderTokenizer tokenizer = new HeaderTokenizer(list, HeaderTokenizer.MIME);
067            while (true) {
068                HeaderTokenizer.Token token = tokenizer.next();
069    
070                switch (token.getType()) {
071                    // the EOF token terminates parsing.
072                    case HeaderTokenizer.Token.EOF:
073                        return;
074    
075                    // each new parameter is separated by a semicolon, including the first, which separates
076                    // the parameters from the main part of the header.
077                    case ';':
078                        // the next token needs to be a parameter name
079                        token = tokenizer.next();
080                        // allow a trailing semicolon on the parameters.
081                        if (token.getType() == HeaderTokenizer.Token.EOF) {
082                            return;
083                        }
084    
085                        if (token.getType() != HeaderTokenizer.Token.ATOM) {
086                            throw new ParseException("Invalid parameter name: " + token.getValue());
087                        }
088    
089                        // get the parameter name as a lower case version for better mapping.
090                        String name = token.getValue().toLowerCase();
091    
092                        token = tokenizer.next();
093    
094                        // parameters are name=value, so we must have the "=" here.
095                        if (token.getType() != '=') {
096                            throw new ParseException("Missing '='");
097                        }
098    
099                        // now the value, which may be an atom or a literal
100                        token = tokenizer.next();
101    
102                        if (token.getType() != HeaderTokenizer.Token.ATOM && token.getType() != HeaderTokenizer.Token.QUOTEDSTRING) {
103                            throw new ParseException("Invalid parameter value: " + token.getValue());
104                        }
105    
106                        String value = token.getValue();
107                        String decodedValue = null;
108    
109                        // we might have to do some additional decoding.  A name that ends with "*"
110                        // is marked as being encoded, so if requested, we decode the value.
111                        if (decodeParameters && name.endsWith("*")) {
112                            // the name needs to be pruned of the marker, and we need to decode the value.
113                            name = name.substring(0, name.length() - 1);
114                            // get a new decoder
115                            RFC2231Encoder decoder = new RFC2231Encoder(HeaderTokenizer.MIME);
116    
117                            try {
118                                // decode the value
119                                decodedValue = decoder.decode(value);
120                            } catch (Exception e) {
121                                // if we're doing things strictly, then raise a parsing exception for errors.
122                                // otherwise, leave the value in its current state.
123                                if (decodeParametersStrict) {
124                                    throw new ParseException("Invalid RFC2231 encoded parameter");
125                                }
126                            }
127                            _parameters.put(name, new ParameterValue(name, decodedValue, value));
128                        }
129                        else {
130                            _parameters.put(name, new ParameterValue(name, value));
131                        }
132    
133                        break;
134    
135                    default:
136                        throw new ParseException("Missing ';'");
137    
138                }
139            }
140        }
141    
142        /**
143         * Get the initial parameters that control parsing and values.
144         * These parameters are controlled by System properties.
145         */
146        private void getInitialProperties() {
147            decodeParameters = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS, false);
148            decodeParametersStrict = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS_STRICT, false);
149            encodeParameters = SessionUtil.getBooleanProperty(MIME_ENCODEPARAMETERS, true);
150        }
151    
152        public int size() {
153            return _parameters.size();
154        }
155    
156        public String get(String name) {
157            ParameterValue value = (ParameterValue)_parameters.get(name.toLowerCase());
158            if (value != null) {
159                return value.value;
160            }
161            return null;
162        }
163    
164        public void set(String name, String value) {
165            name = name.toLowerCase();
166            _parameters.put(name, new ParameterValue(name, value));
167        }
168    
169        public void set(String name, String value, String charset) {
170            name = name.toLowerCase();
171            // only encode if told to and this contains non-ASCII charactes.
172            if (encodeParameters && !ASCIIUtil.isAscii(value)) {
173                ByteArrayOutputStream out = new ByteArrayOutputStream();
174    
175                try {
176                    RFC2231Encoder encoder = new RFC2231Encoder(HeaderTokenizer.MIME);
177    
178                    // extract the bytes using the given character set and encode
179                    byte[] valueBytes = value.getBytes(MimeUtility.javaCharset(charset));
180    
181                    // the string format is charset''data
182                    out.write(charset.getBytes());
183                    out.write('\'');
184                    out.write('\'');
185                    encoder.encode(valueBytes, 0, valueBytes.length, out);
186    
187                    // default in case there is an exception
188                    _parameters.put(name, new ParameterValue(name, value, new String(out.toByteArray())));
189                    return;
190    
191                } catch (Exception e) {
192                    // just fall through and set the value directly if there is an error
193                }
194            }
195            // default in case there is an exception
196            _parameters.put(name, new ParameterValue(name, value));
197        }
198    
199        public void remove(String name) {
200            _parameters.remove(name);
201        }
202    
203        public Enumeration getNames() {
204            return Collections.enumeration(_parameters.keySet());
205        }
206    
207        public String toString() {
208            // we need to perform folding, but out starting point is 0.
209            return toString(0);
210        }
211    
212        public String toString(int used) {
213            StringBuffer stringValue = new StringBuffer();
214    
215            Iterator values = _parameters.values().iterator();
216    
217            while (values.hasNext()) {
218                ParameterValue parm = (ParameterValue)values.next();
219                // get the values we're going to encode in here.
220                String name = parm.getEncodedName();
221                String value = parm.toString();
222    
223                // add the semicolon separator.  We also add a blank so that folding/unfolding rules can be used.
224                stringValue.append("; ");
225                used += 2;
226    
227                // too big for the current header line?
228                if ((used + name.length() + value.length() + 1) > HEADER_SIZE_LIMIT) {
229                    // and a CRLF-combo combo.
230                    stringValue.append("\r\n\t");
231                    // reset the counter for a fresh line
232                    // note we use use 8 because we're using a rather than a blank 
233                    used = 8;
234                }
235                // now add the keyword/value pair.
236                stringValue.append(name);
237                stringValue.append("=");
238    
239                used += name.length() + 1;
240    
241                // we're not out of the woods yet.  It is possible that the keyword/value pair by itself might
242                // be too long for a single line.  If that's the case, the we need to fold the value, if possible
243                if (used + value.length() > HEADER_SIZE_LIMIT) {
244                    String foldedValue = MimeUtility.fold(used, value);
245    
246                    stringValue.append(foldedValue);
247    
248                    // now we need to sort out how much of the current line is in use.
249                    int lastLineBreak = foldedValue.lastIndexOf('\n');
250    
251                    if (lastLineBreak != -1) {
252                        used = foldedValue.length() - lastLineBreak + 1;
253                    }
254                    else {
255                        used += foldedValue.length();
256                    }
257                }
258                else {
259                    // no folding required, just append.
260                    stringValue.append(value);
261                    used += value.length();
262                }
263            }
264    
265            return stringValue.toString();
266        }
267    
268    
269        /**
270         * Utility class for representing parameter values in the list.
271         */
272        class ParameterValue {
273            public String name;              // the name of the parameter
274            public String value;             // the original set value
275            public String encodedValue;      // an encoded value, if encoding is requested.
276    
277            public ParameterValue(String name, String value) {
278                this.name = name;
279                this.value = value;
280                this.encodedValue = null;
281            }
282    
283            public ParameterValue(String name, String value, String encodedValue) {
284                this.name = name;
285                this.value = value;
286                this.encodedValue = encodedValue;
287            }
288    
289            public String toString() {
290                if (encodedValue != null) {
291                    return MimeUtility.quote(encodedValue, HeaderTokenizer.MIME);
292                }
293                return MimeUtility.quote(value, HeaderTokenizer.MIME);
294            }
295    
296            public String getEncodedName() {
297                if (encodedValue != null) {
298                    return name + "*";
299                }
300                return name;
301            }
302        }
303    }