1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package org.apache.geronimo.mail.util;
21
22 import java.io.ByteArrayOutputStream;
23 import java.io.IOException;
24 import java.io.OutputStream;
25 import java.io.UnsupportedEncodingException;
26
27 import javax.mail.internet.MimeUtility;
28
29 /**
30 * Encoder for RFC2231 encoded parameters
31 *
32 * RFC2231 string are encoded as
33 *
34 * charset'language'encoded-text
35 *
36 * and
37 *
38 * encoded-text = *(char / hexchar)
39 *
40 * where
41 *
42 * char is any ASCII character in the range 33-126, EXCEPT
43 * the characters "%" and " ".
44 *
45 * hexchar is an ASCII "%" followed by two upper case
46 * hexadecimal digits.
47 */
48
49 public class RFC2231Encoder implements Encoder
50 {
51 protected final byte[] encodingTable =
52 {
53 (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7',
54 (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F'
55 };
56
57 protected String DEFAULT_SPECIALS = " *'%";
58 protected String specials = DEFAULT_SPECIALS;
59
60 /*
61 * set up the decoding table.
62 */
63 protected final byte[] decodingTable = new byte[128];
64
65 protected void initialiseDecodingTable()
66 {
67 for (int i = 0; i < encodingTable.length; i++)
68 {
69 decodingTable[encodingTable[i]] = (byte)i;
70 }
71 }
72
73 public RFC2231Encoder()
74 {
75 this(null);
76 }
77
78 public RFC2231Encoder(String specials)
79 {
80 if (specials != null) {
81 this.specials = DEFAULT_SPECIALS + specials;
82 }
83 initialiseDecodingTable();
84 }
85
86
87 /**
88 * encode the input data producing an RFC2231 output stream.
89 *
90 * @return the number of bytes produced.
91 */
92 public int encode(byte[] data, int off, int length, OutputStream out) throws IOException {
93
94 int bytesWritten = 0;
95 for (int i = off; i < (off + length); i++)
96 {
97 int ch = data[i] & 0xff;
98 // character tha must be encoded? Prefix with a '%' and encode in hex.
99 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 }