2 // UTF-7 codec, according to https://tools.ietf.org/html/rfc2152
3 // Below is UTF-7-IMAP codec, according to http://tools.ietf.org/html/rfc3501#section-5.1.3
5 exports.utf7 = function(options) {
7 encoder: function utf7Encoder() {
9 write: utf7EncoderWrite,
15 decoder: function utf7Decoder() {
17 write: utf7DecoderWrite,
29 var nonDirectChars = /[^A-Za-z0-9'\(\),-\.\/:\? \n\r\t]+/g;
31 // Direct: \n, \r, \t, 0x20, 0x27-0x29, (0x2B), 0x2C-0x3A, 0x3F, 0x41-0x5A, 0x61-0x7A
33 function utf7EncoderWrite(str) {
34 // Naive implementation.
35 // Non-direct chars are encoded as "+<base64>-"; single "+" char is encoded as "+-".
36 return new Buffer(str.replace(nonDirectChars, function(chunk) {
37 return "+" + (chunk === '+' ? '' :
38 this.iconv.encode(chunk, 'utf16-be').toString('base64').replace(/=+$/, ''))
44 var base64Regex = /[A-Za-z0-9\/+]/;
46 for (var i = 0; i < 256; i++)
47 base64Chars[i] = base64Regex.test(String.fromCharCode(i));
49 var plusChar = '+'.charCodeAt(0),
50 minusChar = '-'.charCodeAt(0),
51 andChar = '&'.charCodeAt(0);
53 function utf7DecoderWrite(buf) {
54 var res = "", lastI = 0,
55 inBase64 = this.inBase64,
56 base64Accum = this.base64Accum;
58 // The decoder is more involved as we must handle chunks in stream.
60 for (var i = 0; i < buf.length; i++) {
61 if (!inBase64) { // We're in direct mode.
62 // Write direct chars until '+'
63 if (buf[i] == plusChar) {
64 res += this.iconv.decode(buf.slice(lastI, i), "ascii"); // Write direct chars.
68 } else { // We decode base64.
69 if (!base64Chars[buf[i]]) { // Base64 ended.
70 if (i == lastI && buf[i] == minusChar) {// "+-" -> "+"
73 var b64str = base64Accum + buf.slice(lastI, i).toString();
74 res += this.iconv.decode(new Buffer(b64str, 'base64'), "utf16-be");
77 if (buf[i] != minusChar) // Minus is absorbed after base64.
88 res += this.iconv.decode(buf.slice(lastI), "ascii"); // Write direct chars.
90 var b64str = base64Accum + buf.slice(lastI).toString();
92 var canBeDecoded = b64str.length - (b64str.length % 8); // Minimal chunk: 2 quads -> 2x3 bytes -> 3 chars.
93 base64Accum = b64str.slice(canBeDecoded); // The rest will be decoded in future.
94 b64str = b64str.slice(0, canBeDecoded);
96 res += this.iconv.decode(new Buffer(b64str, 'base64'), "utf16-be");
99 this.inBase64 = inBase64;
100 this.base64Accum = base64Accum;
105 function utf7DecoderEnd() {
107 if (this.inBase64 && this.base64Accum.length > 0)
108 res = this.iconv.decode(new Buffer(this.base64Accum, 'base64'), "utf16-be");
110 this.inBase64 = false;
111 this.base64Accum = '';
117 // RFC3501 Sec. 5.1.3 Modified UTF-7 (http://tools.ietf.org/html/rfc3501#section-5.1.3)
119 // * Base64 part is started by "&" instead of "+"
120 // * Direct characters are 0x20-0x7E, except "&" (0x26)
121 // * In Base64, "," is used instead of "/"
122 // * Base64 must not be used to represent direct characters.
123 // * No implicit shift back from Base64 (should always end with '-')
124 // * String must end in non-shifted position.
125 // * "-&" while in base64 is not allowed.
128 exports.utf7imap = function(options) {
130 encoder: function utf7ImapEncoder() {
132 write: utf7ImapEncoderWrite,
133 end: utf7ImapEncoderEnd,
135 iconv: options.iconv,
137 base64Accum: new Buffer(6),
141 decoder: function utf7ImapDecoder() {
143 write: utf7ImapDecoderWrite,
144 end: utf7ImapDecoderEnd,
146 iconv: options.iconv,
155 function utf7ImapEncoderWrite(str) {
156 var inBase64 = this.inBase64,
157 base64Accum = this.base64Accum,
158 base64AccumIdx = this.base64AccumIdx,
159 buf = new Buffer(str.length*5 + 10), bufIdx = 0;
161 for (var i = 0; i < str.length; i++) {
162 var uChar = str.charCodeAt(i);
163 if (0x20 <= uChar && uChar <= 0x7E) { // Direct character or '&'.
165 if (base64AccumIdx > 0) {
166 bufIdx += buf.write(base64Accum.slice(0, base64AccumIdx).toString('base64').replace(/\//g, ',').replace(/=+$/, ''), bufIdx);
170 buf[bufIdx++] = minusChar; // Write '-', then go to direct mode.
175 buf[bufIdx++] = uChar; // Write direct character
177 if (uChar === andChar) // Ampersand -> '&-'
178 buf[bufIdx++] = minusChar;
181 } else { // Non-direct character
183 buf[bufIdx++] = andChar; // Write '&', then go to base64 mode.
187 base64Accum[base64AccumIdx++] = uChar >> 8;
188 base64Accum[base64AccumIdx++] = uChar & 0xFF;
190 if (base64AccumIdx == base64Accum.length) {
191 bufIdx += buf.write(base64Accum.toString('base64').replace(/\//g, ','), bufIdx);
198 this.inBase64 = inBase64;
199 this.base64AccumIdx = base64AccumIdx;
201 return buf.slice(0, bufIdx);
204 function utf7ImapEncoderEnd() {
205 var buf = new Buffer(10), bufIdx = 0;
207 if (this.base64AccumIdx > 0) {
208 bufIdx += buf.write(this.base64Accum.slice(0, this.base64AccumIdx).toString('base64').replace(/\//g, ',').replace(/=+$/, ''), bufIdx);
209 this.base64AccumIdx = 0;
212 buf[bufIdx++] = minusChar; // Write '-', then go to direct mode.
213 this.inBase64 = false;
216 return buf.slice(0, bufIdx);
220 var base64IMAPChars = base64Chars.slice();
221 base64IMAPChars[','.charCodeAt(0)] = true;
223 function utf7ImapDecoderWrite(buf) {
224 var res = "", lastI = 0,
225 inBase64 = this.inBase64,
226 base64Accum = this.base64Accum;
228 // The decoder is more involved as we must handle chunks in stream.
229 // It is forgiving, closer to standard UTF-7 (for example, '-' is optional at the end).
231 for (var i = 0; i < buf.length; i++) {
232 if (!inBase64) { // We're in direct mode.
233 // Write direct chars until '&'
234 if (buf[i] == andChar) {
235 res += this.iconv.decode(buf.slice(lastI, i), "ascii"); // Write direct chars.
239 } else { // We decode base64.
240 if (!base64IMAPChars[buf[i]]) { // Base64 ended.
241 if (i == lastI && buf[i] == minusChar) { // "&-" -> "&"
244 var b64str = base64Accum + buf.slice(lastI, i).toString().replace(/,/g, '/');
245 res += this.iconv.decode(new Buffer(b64str, 'base64'), "utf16-be");
248 if (buf[i] != minusChar) // Minus may be absorbed after base64.
259 res += this.iconv.decode(buf.slice(lastI), "ascii"); // Write direct chars.
261 var b64str = base64Accum + buf.slice(lastI).toString().replace(/,/g, '/');
263 var canBeDecoded = b64str.length - (b64str.length % 8); // Minimal chunk: 2 quads -> 2x3 bytes -> 3 chars.
264 base64Accum = b64str.slice(canBeDecoded); // The rest will be decoded in future.
265 b64str = b64str.slice(0, canBeDecoded);
267 res += this.iconv.decode(new Buffer(b64str, 'base64'), "utf16-be");
270 this.inBase64 = inBase64;
271 this.base64Accum = base64Accum;
276 function utf7ImapDecoderEnd() {
278 if (this.inBase64 && this.base64Accum.length > 0)
279 res = this.iconv.decode(new Buffer(this.base64Accum, 'base64'), "utf16-be");
281 this.inBase64 = false;
282 this.base64Accum = '';