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 }