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