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 org.apache.geronimo.mail.util;
021    
022    import java.io.ByteArrayOutputStream;
023    import java.io.IOException;
024    import java.io.OutputStream;
025    import java.io.UnsupportedEncodingException;
026    
027    import javax.mail.internet.MimeUtility;
028    
029    /**
030     * Encoder for RFC2231 encoded parameters
031     *
032     * RFC2231 string are encoded as
033     *
034     *    charset'language'encoded-text
035     *
036     * and
037     *
038     *    encoded-text = *(char / hexchar)
039     *
040     * where
041     *
042     *    char is any ASCII character in the range 33-126, EXCEPT
043     *    the characters "%" and " ".
044     *
045     *    hexchar is an ASCII "%" followed by two upper case
046     *    hexadecimal digits.
047     */
048    
049    public class RFC2231Encoder implements Encoder
050    {
051        protected final byte[] encodingTable =
052            {
053                (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7',
054                (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F'
055            };
056    
057        protected String DEFAULT_SPECIALS = " *'%";
058        protected String specials = DEFAULT_SPECIALS;
059    
060        /*
061         * set up the decoding table.
062         */
063        protected final byte[] decodingTable = new byte[128];
064    
065        protected void initialiseDecodingTable()
066        {
067            for (int i = 0; i < encodingTable.length; i++)
068            {
069                decodingTable[encodingTable[i]] = (byte)i;
070            }
071        }
072    
073        public RFC2231Encoder()
074        {
075            this(null);
076        }
077    
078        public RFC2231Encoder(String specials)
079        {
080            if (specials != null) {
081                this.specials = DEFAULT_SPECIALS + specials;
082            }
083            initialiseDecodingTable();
084        }
085    
086    
087        /**
088         * encode the input data producing an RFC2231 output stream.
089         *
090         * @return the number of bytes produced.
091         */
092        public int encode(byte[] data, int off, int length, OutputStream out) throws IOException {
093    
094            int bytesWritten = 0;
095            for (int i = off; i < (off + length); i++)
096            {
097                int ch = data[i] & 0xff;
098                // character tha must be encoded?  Prefix with a '%' and encode in hex.
099                if (ch <= 32 || ch >= 127 || specials.indexOf(ch) != -1) {
100                    out.write((byte)'%');
101                    out.write(encodingTable[ch >> 4]);
102                    out.write(encodingTable[ch & 0xf]);
103                    bytesWritten += 3;
104                }
105                else {
106                    // add unchanged.
107                    out.write((byte)ch);
108                    bytesWritten++;
109                }
110            }
111    
112            return bytesWritten;
113        }
114    
115    
116        /**
117         * decode the RFC2231 encoded byte data writing it to the given output stream
118         *
119         * @return the number of bytes produced.
120         */
121        public int decode(byte[] data, int off, int length, OutputStream out) throws IOException {
122            int        outLen = 0;
123            int        end = off + length;
124    
125            int i = off;
126            while (i < end)
127            {
128                byte v = data[i++];
129                // a percent is a hex character marker, need to decode a hex value.
130                if (v == '%') {
131                    byte b1 = decodingTable[data[i++]];
132                    byte b2 = decodingTable[data[i++]];
133                    out.write((b1 << 4) | b2);
134                }
135                else {
136                    // copied over unchanged.
137                    out.write(v);
138                }
139                // always just one byte added
140                outLen++;
141            }
142    
143            return outLen;
144        }
145    
146        /**
147         * decode the RFC2231 encoded String data writing it to the given output stream.
148         *
149         * @return the number of bytes produced.
150         */
151        public int decode(String data, OutputStream out) throws IOException
152        {
153            int        length = 0;
154            int        end = data.length();
155    
156            int i = 0;
157            while (i < end)
158            {
159                char v = data.charAt(i++);
160                if (v == '%') {
161                    byte b1 = decodingTable[data.charAt(i++)];
162                    byte b2 = decodingTable[data.charAt(i++)];
163    
164                    out.write((b1 << 4) | b2);
165                }
166                else {
167                    out.write((byte)v);
168                }
169                length++;
170            }
171    
172            return length;
173        }
174    
175    
176        /**
177         * Encode a string as an RFC2231 encoded parameter, using the
178         * given character set and language.
179         *
180         * @param charset  The source character set (the MIME version).
181         * @param language The encoding language.
182         * @param data     The data to encode.
183         *
184         * @return The encoded string.
185         */
186        public String encode(String charset, String language, String data) throws IOException {
187    
188            byte[] bytes = null;
189            try {
190                // the charset we're adding is the MIME-defined name.  We need the java version
191                // in order to extract the bytes.
192                bytes = data.getBytes(MimeUtility.javaCharset(charset));
193            } catch (UnsupportedEncodingException e) {
194                // we have a translation problem here.
195                return null;
196            }
197    
198            StringBuffer result = new StringBuffer();
199    
200            // append the character set, if we have it.
201            if (charset != null) {
202                result.append(charset);
203            }
204            // the field marker is required.
205            result.append("'");
206    
207            // and the same for the language.
208            if (language != null) {
209                result.append(language);
210            }
211            // the field marker is required.
212            result.append("'");
213    
214            // wrap an output stream around our buffer for the decoding
215            OutputStream out = new StringBufferOutputStream(result);
216    
217            // encode the data stream
218            encode(bytes, 0, bytes.length, out);
219    
220            // finis!
221            return result.toString();
222        }
223    
224    
225        /**
226         * Decode an RFC2231 encoded string.
227         *
228         * @param data   The data to decode.
229         *
230         * @return The decoded string.
231         * @exception IOException
232         * @exception UnsupportedEncodingException
233         */
234        public String decode(String data) throws IOException, UnsupportedEncodingException {
235            // get the end of the language field
236            int charsetEnd = data.indexOf('\'');
237            // uh oh, might not be there
238            if (charsetEnd == -1) {
239                throw new IOException("Missing charset in RFC2231 encoded value");
240            }
241    
242            String charset = data.substring(0, charsetEnd);
243    
244            // now pull out the language the same way
245            int languageEnd = data.indexOf('\'', charsetEnd + 1);
246            if (languageEnd == -1) {
247                throw new IOException("Missing language in RFC2231 encoded value");
248            }
249    
250            String language = data.substring(charsetEnd + 1, languageEnd);
251    
252            ByteArrayOutputStream out = new ByteArrayOutputStream(data.length());
253    
254            // decode the data
255            decode(data.substring(languageEnd + 1), out);
256    
257            byte[] bytes = out.toByteArray();
258            // build a new string from this using the java version of the encoded charset.
259            return new String(bytes, 0, bytes.length, MimeUtility.javaCharset(charset));
260        }
261    }