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 }