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