Skip to content

Package: ParameterList$MultiValue

ParameterList$MultiValue

Coverage

1: /*
2: * Copyright (c) 1997, 2021 Oracle and/or its affiliates. All rights reserved.
3: *
4: * This program and the accompanying materials are made available under the
5: * terms of the Eclipse Public License v. 2.0, which is available at
6: * http://www.eclipse.org/legal/epl-2.0.
7: *
8: * This Source Code may also be made available under the following Secondary
9: * Licenses when the conditions for such availability set forth in the
10: * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11: * version 2 with the GNU Classpath Exception, which is available at
12: * https://www.gnu.org/software/classpath/license.html.
13: *
14: * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15: */
16:
17: package jakarta.mail.internet;
18:
19: import jakarta.mail.internet.MimeUtility;
20:
21: import java.io.ByteArrayOutputStream;
22: import java.io.IOException;
23: import java.io.OutputStream;
24: import java.io.UnsupportedEncodingException;
25: import java.util.ArrayList;
26: import java.util.Enumeration;
27: import java.util.HashMap;
28: import java.util.HashSet;
29: import java.util.Iterator;
30: import java.util.LinkedHashMap;
31: import java.util.Locale;
32: import java.util.Map;
33: import java.util.Set;
34:
35: /**
36: * This class holds MIME parameters (attribute-value pairs).
37: * The <code>mail.mime.encodeparameters</code> and
38: * <code>mail.mime.decodeparameters</code> System properties
39: * control whether encoded parameters, as specified by
40: * <a href="http://www.ietf.org/rfc/rfc2231.txt" target="_top">RFC 2231</a>,
41: * are supported. By default, such encoded parameters <b>are</b>
42: * supported. <p>
43: *
44: * Also, in the current implementation, setting the System property
45: * <code>mail.mime.decodeparameters.strict</code> to <code>"true"</code>
46: * will cause a <code>ParseException</code> to be thrown for errors
47: * detected while decoding encoded parameters. By default, if any
48: * decoding errors occur, the original (undecoded) string is used. <p>
49: *
50: * The current implementation supports the System property
51: * <code>mail.mime.parameters.strict</code>, which if set to false
52: * when parsing a parameter list allows parameter values
53: * to contain whitespace and other special characters without
54: * being quoted; the parameter value ends at the next semicolon.
55: * If set to true (the default), parameter values are required to conform
56: * to the MIME specification and must be quoted if they contain whitespace
57: * or special characters.
58: *
59: * @author John Mani
60: * @author Bill Shannon
61: */
62:
63: public class ParameterList {
64:
65: /**
66: * The map of name, value pairs.
67: * The value object is either a String, for unencoded
68: * values, or a Value object, for encoded values,
69: * or a MultiValue object, for multi-segment parameters,
70: * or a LiteralValue object for strings that should not be encoded.
71: *
72: * We use a LinkedHashMap so that parameters are (as much as
73: * possible) kept in the original order. Note however that
74: * multi-segment parameters (see below) will appear in the
75: * position of the first seen segment and orphan segments
76: * will all move to the end.
77: */
78: // keep parameters in order
79: private Map<String, Object> list = new LinkedHashMap<>();
80:
81: /**
82: * A set of names for multi-segment parameters that we
83: * haven't processed yet. Normally such names are accumulated
84: * during the inital parse and processed at the end of the parse,
85: * but such names can also be set via the set method when the
86: * IMAP provider accumulates pre-parsed pieces of a parameter list.
87: * (A special call to the set method tells us when the IMAP provider
88: * is done setting parameters.)
89: *
90: * A multi-segment parameter is defined by RFC 2231. For example,
91: * "title*0=part1; title*1=part2", which represents a parameter
92: * named "title" with value "part1part2".
93: *
94: * Note also that each segment of the value might or might not be
95: * encoded, indicated by a trailing "*" on the parameter name.
96: * If any segment is encoded, the first segment must be encoded.
97: * Only the first segment contains the charset and language
98: * information needed to decode any encoded segments.
99: *
100: * RFC 2231 introduces many possible failure modes, which we try
101: * to handle as gracefully as possible. Generally, a failure to
102: * decode a parameter value causes the non-decoded parameter value
103: * to be used instead. Missing segments cause all later segments
104: * to be appear as independent parameters with names that include
105: * the segment number. For example, "title*0=part1; title*1=part2;
106: * title*3=part4" appears as two parameters named "title" and "title*3".
107: */
108: private Set<String> multisegmentNames;
109:
110: /**
111: * A map containing the segments for all not-yet-processed
112: * multi-segment parameters. The map is indexed by "name*seg".
113: * The value object is either a String or a Value object.
114: * The Value object is not decoded during the initial parse
115: * because the segments may appear in any order and until the
116: * first segment appears we don't know what charset to use to
117: * decode the encoded segments. The segments are hex decoded
118: * in order, combined into a single byte array, and converted
119: * to a String using the specified charset in the
120: * combineMultisegmentNames method.
121: */
122: private Map<String, Object> slist;
123:
124: /**
125: * MWB 3BView: The name of the last parameter added to the map.
126: * Used for the AppleMail hack.
127: */
128: private String lastName = null;
129:
130: private static final boolean encodeParameters =
131:         MimeUtility.getBooleanSystemProperty("mail.mime.encodeparameters", true);
132: private static final boolean decodeParameters =
133:         MimeUtility.getBooleanSystemProperty("mail.mime.decodeparameters", true);
134: private static final boolean decodeParametersStrict =
135:         MimeUtility.getBooleanSystemProperty(
136:          "mail.mime.decodeparameters.strict", false);
137: private static final boolean applehack =
138:         MimeUtility.getBooleanSystemProperty("mail.mime.applefilenames", false);
139: private static final boolean windowshack =
140:         MimeUtility.getBooleanSystemProperty("mail.mime.windowsfilenames", false);
141: private static final boolean parametersStrict =
142:         MimeUtility.getBooleanSystemProperty("mail.mime.parameters.strict", true);
143: private static final boolean splitLongParameters =
144:         MimeUtility.getBooleanSystemProperty(
145:          "mail.mime.splitlongparameters", true);
146:
147:
148: /**
149: * A struct to hold an encoded value.
150: * A parsed encoded value is stored as both the
151: * decoded value and the original encoded value
152: * (so that toString will produce the same result).
153: * An encoded value that is set explicitly is stored
154: * as the original value and the encoded value, to
155: * ensure that get will return the same value that
156: * was set.
157: */
158: private static class Value {
159:         String value;
160:         String charset;
161:         String encodedValue;
162: }
163:
164: /**
165: * A struct to hold a literal value that shouldn't be further encoded.
166: */
167: private static class LiteralValue {
168:         String value;
169: }
170:
171: /**
172: * A struct for a multi-segment parameter. Each entry in the
173: * List is either a String or a Value object. When all the
174: * segments are present and combined in the combineMultisegmentNames
175: * method, the value field contains the combined and decoded value.
176: * Until then the value field contains an empty string as a placeholder.
177: */
178: private static class MultiValue extends ArrayList<Object> {
179:         // keep lint happy
180:         private static final long serialVersionUID = 699561094618751023L;
181:
182:         String value;
183: }
184:
185: /**
186: * Map the LinkedHashMap's keySet iterator to an Enumeration.
187: */
188: private static class ParamEnum implements Enumeration<String> {
189:         private Iterator<String> it;
190:
191:         ParamEnum(Iterator<String> it) {
192:          this.it = it;
193:         }
194:
195:         @Override
196:         public boolean hasMoreElements() {
197:          return it.hasNext();
198:         }
199:
200:         @Override
201:         public String nextElement() {
202:          return it.next();
203:         }
204: }
205:
206: /**
207: * No-arg Constructor.
208: */
209: public ParameterList() {
210:         // initialize other collections only if they'll be needed
211:         if (decodeParameters) {
212:          multisegmentNames = new HashSet<>();
213:          slist = new HashMap<>();
214:         }
215: }
216:
217: /**
218: * Constructor that takes a parameter-list string. The String
219: * is parsed and the parameters are collected and stored internally.
220: * A ParseException is thrown if the parse fails.
221: * Note that an empty parameter-list string is valid and will be
222: * parsed into an empty ParameterList.
223: *
224: * @param        s        the parameter-list string.
225: * @exception        ParseException if the parse fails.
226: */
227: public ParameterList(String s) throws ParseException {
228:         this();
229:
230:         HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME);
231:         for (;;) {
232:          HeaderTokenizer.Token tk = h.next();
233:          int type = tk.getType();
234:          String name, value;
235:
236:          if (type == HeaderTokenizer.Token.EOF) // done
237:                 break;
238:
239:          if ((char)type == ';') {
240:                 // expect parameter name
241:                 tk = h.next();
242:                 // tolerate trailing semicolon, even though it violates the spec
243:                 if (tk.getType() == HeaderTokenizer.Token.EOF)
244:                  break;
245:                 // parameter name must be a MIME Atom
246:                 if (tk.getType() != HeaderTokenizer.Token.ATOM)
247:                  throw new ParseException("In parameter list <" + s + ">" +
248:                                          ", expected parameter name, " +
249:                                          "got \"" + tk.getValue() + "\"");
250:                 name = tk.getValue().toLowerCase(Locale.ENGLISH);
251:
252:                 // expect '='
253:                 tk = h.next();
254:                 if ((char)tk.getType() != '=')
255:                  throw new ParseException("In parameter list <" + s + ">" +
256:                                          ", expected '=', " +
257:                                          "got \"" + tk.getValue() + "\"");
258:
259:                 // expect parameter value
260:                 if (windowshack &&
261:                         (name.equals("name") || name.equals("filename")))
262:                  tk = h.next(';', true);
263:                 else if (parametersStrict)
264:                  tk = h.next();
265:                 else
266:                  tk = h.next(';');
267:                 type = tk.getType();
268:                 // parameter value must be a MIME Atom or Quoted String
269:                 if (type != HeaderTokenizer.Token.ATOM &&
270:                  type != HeaderTokenizer.Token.QUOTEDSTRING)
271:                  throw new ParseException("In parameter list <" + s + ">" +
272:                                          ", expected parameter value, " +
273:                                          "got \"" + tk.getValue() + "\"");
274:
275:                 value = tk.getValue();
276:                 lastName = name;
277:                 if (decodeParameters)
278:                  putEncodedName(name, value);
279:                 else
280:                  list.put(name, value);
281: } else {
282:                 // MWB 3BView new code to add in filenames generated by
283:                 // AppleMail.
284:                 // Note - one space is assumed between name elements.
285:                 // This may not be correct but it shouldn't matter too much.
286:                 // Note: AppleMail encodes filenames with non-ascii characters
287:                 // correctly, so we don't need to worry about the name* subkeys.
288:                 if (type == HeaderTokenizer.Token.ATOM && lastName != null &&
289:                          ((applehack &&
290:                                 (lastName.equals("name") ||
291:                                  lastName.equals("filename"))) ||
292:                          !parametersStrict)
293:                          ) {
294:                  // Add value to previous value
295:                  String lastValue = (String)list.get(lastName);
296:                  value = lastValue + " " + tk.getValue();
297:                  list.put(lastName, value);
298: } else {
299:                  throw new ParseException("In parameter list <" + s + ">" +
300:                                          ", expected ';', got \"" +
301:                                          tk.getValue() + "\"");
302:                 }
303:          }
304: }
305:
306:         if (decodeParameters) {
307:          /*
308:          * After parsing all the parameters, combine all the
309:          * multi-segment parameter values together.
310:          */
311:          combineMultisegmentNames(false);
312:         }
313: }
314:
315: /**
316: * Normal users of this class will use simple parameter names.
317: * In some cases, for example, when processing IMAP protocol
318: * messages, individual segments of a multi-segment name
319: * (specified by RFC 2231) will be encountered and passed to
320: * the {@link #set} method. After all these segments are added
321: * to this ParameterList, they need to be combined to represent
322: * the logical parameter name and value. This method will combine
323: * all segments of multi-segment names. <p>
324: *
325: * Normal users should never need to call this method.
326: *
327: * @since        JavaMail 1.5
328: */
329: public void combineSegments() {
330:         /*
331:          * If we've accumulated any multi-segment names from calls to
332:          * the set method from (e.g.) the IMAP provider, combine the pieces.
333:          * Ignore any parse errors (e.g., from decoding the values)
334:          * because it's too late to report them.
335:          */
336:         if (decodeParameters && multisegmentNames.size() > 0) {
337:          try {
338:                 combineMultisegmentNames(true);
339:          } catch (ParseException pex) {
340:                 // too late to do anything about it
341:          }
342:         }
343: }
344:
345: /**
346: * If the name is an encoded or multi-segment name (or both)
347: * handle it appropriately, storing the appropriate String
348: * or Value object. Multi-segment names are stored in the
349: * main parameter list as an emtpy string as a placeholder,
350: * replaced later in combineMultisegmentNames with a MultiValue
351: * object. This causes all pieces of the multi-segment parameter
352: * to appear in the position of the first seen segment of the
353: * parameter.
354: */
355: private void putEncodedName(String name, String value)
356:                                 throws ParseException {
357:         int star = name.indexOf('*');
358:         if (star < 0) {
359:          // single parameter, unencoded value
360:          list.put(name, value);
361:         } else if (star == name.length() - 1) {
362:          // single parameter, encoded value
363:          name = name.substring(0, star);
364:          Value v = extractCharset(value);
365:          try {
366:                 v.value = decodeBytes(v.value, v.charset);
367:          } catch (UnsupportedEncodingException ex) {
368:                 if (decodeParametersStrict)
369:                  throw new ParseException(ex.toString());
370:          }
371:          list.put(name, v);
372:         } else {
373:          // multiple segments
374:          String rname = name.substring(0, star);
375:          multisegmentNames.add(rname);
376:          list.put(rname, "");
377:
378:          Object v;
379:          if (name.endsWith("*")) {
380:                 // encoded value
381:                 if (name.endsWith("*0*")) {        // first segment
382:                  v = extractCharset(value);
383:                 } else {
384:                  v = new Value();
385:                  ((Value)v).encodedValue = value;
386:                  ((Value)v).value = value;        // default; decoded later
387:                 }
388:                 name = name.substring(0, name.length() - 1);
389:          } else {
390:                 // unencoded value
391:                 v = value;
392:          }
393:          slist.put(name, v);
394:         }
395: }
396:
397: /**
398: * Iterate through the saved set of names of multi-segment parameters,
399: * for each parameter find all segments stored in the slist map,
400: * decode each segment as needed, combine the segments together into
401: * a single decoded value, and save all segments in a MultiValue object
402: * in the main list indexed by the parameter name.
403: */
404: private void combineMultisegmentNames(boolean keepConsistentOnFailure)
405:                                 throws ParseException {
406:         boolean success = false;
407:         try {
408:          Iterator<String> it = multisegmentNames.iterator();
409:          while (it.hasNext()) {
410:                 String name = it.next();
411:                 MultiValue mv = new MultiValue();
412:                 /*
413:                  * Now find all the segments for this name and
414:                  * decode each segment as needed.
415:                  */
416:                 String charset = null;
417:                 ByteArrayOutputStream bos = new ByteArrayOutputStream();
418:                 int segment;
419:                 for (segment = 0; ; segment++) {
420:                  String sname = name + "*" + segment;
421:                  Object v = slist.get(sname);
422:                  if (v == null)        // out of segments
423:                         break;
424:                  mv.add(v);
425:                  try {
426:                         if (v instanceof Value) {
427:                          Value vv = (Value)v;
428:                          if (segment == 0) {
429:                                 // the first segment specifies the charset
430:                                 // for all other encoded segments
431:                                 charset = vv.charset;
432:                          } else {
433:                                 if (charset == null) {
434:                                  // should never happen
435:                                  multisegmentNames.remove(name);
436:                                  break;
437:                                 }
438:                          }
439:                          decodeBytes(vv.value, bos);
440:                         } else {
441:                          bos.write(MimeUtility.getBytes((String)v));
442:                         }
443:                  } catch (IOException ex) {
444:                         // XXX - should never happen
445:                  }
446:                  slist.remove(sname);
447:                 }
448:                 if (segment == 0) {
449:                  // didn't find any segments at all
450:                  list.remove(name);
451:                 } else {
452:                  try {
453:                         if (charset != null)
454:                          charset = MimeUtility.javaCharset(charset);
455:                         if (charset == null || charset.length() == 0)
456:                          charset = MimeUtility.getDefaultJavaCharset();
457:                         if (charset != null)
458:                          mv.value = bos.toString(charset);
459:                         else
460:                          mv.value = bos.toString();
461:                  } catch (UnsupportedEncodingException uex) {
462:                         if (decodeParametersStrict)
463:                          throw new ParseException(uex.toString());
464:                         // convert as if iso-8859-1
465:                         try {
466:                          mv.value = bos.toString("iso-8859-1");
467:                         } catch (UnsupportedEncodingException ex) {
468:                          // should never happen
469:                         }
470:                  }
471:                  list.put(name, mv);
472:                 }
473:          }
474:          success = true;
475:         } finally {
476:          /*
477:          * If we get here because of an exception that's going to
478:          * be thrown (success == false) from the constructor
479:          * (keepConsistentOnFailure == false), this is all wasted effort.
480:          */
481:          if (keepConsistentOnFailure || success) {
482:                 // we should never end up with anything in slist,
483:                 // but if we do, add it all to list
484:                 if (slist.size() > 0) {
485:                  // first, decode any values that we'll add to the list
486:                  Iterator<Object> sit = slist.values().iterator();
487:                  while (sit.hasNext()) {
488:                         Object v = sit.next();
489:                         if (v instanceof Value) {
490:                          Value vv = (Value)v;
491:                          try {
492:                                 vv.value =
493:                                  decodeBytes(vv.value, vv.charset);
494:                          } catch (UnsupportedEncodingException ex) {
495:                                 if (decodeParametersStrict)
496:                                  throw new ParseException(ex.toString());
497:                          }
498:                         }
499:                  }
500:                  list.putAll(slist);
501:                 }
502:
503:                 // clear out the set of names and segments
504:                 multisegmentNames.clear();
505:                 slist.clear();
506:          }
507:         }
508: }
509:
510: /**
511: * Return the number of parameters in this list.
512: *
513: * @return number of parameters.
514: */
515: public int size() {
516:         return list.size();
517: }
518:
519: /**
520: * Returns the value of the specified parameter. Note that
521: * parameter names are case-insensitive.
522: *
523: * @param name        parameter name.
524: * @return                Value of the parameter. Returns
525: *                        <code>null</code> if the parameter is not
526: *                        present.
527: */
528: public String get(String name) {
529:         String value;
530:         Object v = list.get(name.trim().toLowerCase(Locale.ENGLISH));
531:         if (v instanceof MultiValue)
532:          value = ((MultiValue)v).value;
533:         else if (v instanceof LiteralValue)
534:          value = ((LiteralValue)v).value;
535:         else if (v instanceof Value)
536:          value = ((Value)v).value;
537:         else
538:          value = (String)v;
539:         return value;
540: }
541:
542: /**
543: * Set a parameter. If this parameter already exists, it is
544: * replaced by this new value.
545: *
546: * @param        name         name of the parameter.
547: * @param        value        value of the parameter.
548: */
549: public void set(String name, String value) {
550:         name = name.trim().toLowerCase(Locale.ENGLISH);
551:         if (decodeParameters) {
552:          try {
553:                 putEncodedName(name, value);
554:          } catch (ParseException pex) {
555:                 // ignore it
556:                 list.put(name, value);
557:          }
558:         } else
559:          list.put(name, value);
560: }
561:
562: /**
563: * Set a parameter. If this parameter already exists, it is
564: * replaced by this new value. If the
565: * <code>mail.mime.encodeparameters</code> System property
566: * is true, and the parameter value is non-ASCII, it will be
567: * encoded with the specified charset, as specified by RFC 2231.
568: *
569: * @param        name         name of the parameter.
570: * @param        value        value of the parameter.
571: * @param        charset        charset of the parameter value.
572: * @since        JavaMail 1.4
573: */
574: public void set(String name, String value, String charset) {
575:         if (encodeParameters) {
576:          Value ev = encodeValue(value, charset);
577:          // was it actually encoded?
578:          if (ev != null)
579:                 list.put(name.trim().toLowerCase(Locale.ENGLISH), ev);
580:          else
581:                 set(name, value);
582:         } else
583:          set(name, value);
584: }
585:
586: /**
587: * Package-private method to set a literal value that won't be
588: * further encoded. Used to set the filename parameter when
589: * "mail.mime.encodefilename" is true.
590: *
591: * @param        name         name of the parameter.
592: * @param        value        value of the parameter.
593: */
594: void setLiteral(String name, String value) {
595:         LiteralValue lv = new LiteralValue();
596:         lv.value = value;
597:         list.put(name, lv);
598: }
599:
600: /**
601: * Removes the specified parameter from this ParameterList.
602: * This method does nothing if the parameter is not present.
603: *
604: * @param        name        name of the parameter.
605: */
606: public void remove(String name) {
607:         list.remove(name.trim().toLowerCase(Locale.ENGLISH));
608: }
609:
610: /**
611: * Return an enumeration of the names of all parameters in this
612: * list.
613: *
614: * @return Enumeration of all parameter names in this list.
615: */
616: public Enumeration<String> getNames() {
617:         return new ParamEnum(list.keySet().iterator());
618: }
619:
620: /**
621: * Convert this ParameterList into a MIME String. If this is
622: * an empty list, an empty string is returned.
623: *
624: * @return                String
625: */
626: @Override
627: public String toString() {
628:         return toString(0);
629: }
630:
631: /**
632: * Convert this ParameterList into a MIME String. If this is
633: * an empty list, an empty string is returned.
634: *
635: * The 'used' parameter specifies the number of character positions
636: * already taken up in the field into which the resulting parameter
637: * list is to be inserted. It's used to determine where to fold the
638: * resulting parameter list.
639: *
640: * @param used number of character positions already used, in
641: * the field into which the parameter list is to
642: * be inserted.
643: * @return String
644: */
645: public String toString(int used) {
646: ToStringBuffer sb = new ToStringBuffer(used);
647: Iterator<Map.Entry<String, Object>> e = list.entrySet().iterator();
648:
649: while (e.hasNext()) {
650:          Map.Entry<String, Object> ent = e.next();
651:          String name = ent.getKey();
652:          String value;
653:          Object v = ent.getValue();
654:          if (v instanceof MultiValue) {
655:                 MultiValue vv = (MultiValue)v;
656:                 name += "*";
657:                 for (int i = 0; i < vv.size(); i++) {
658:                  Object va = vv.get(i);
659:                  String ns;
660:                  if (va instanceof Value) {
661:                         ns = name + i + "*";
662:                         value = ((Value)va).encodedValue;
663:                  } else {
664:                         ns = name + i;
665:                         value = (String)va;
666:                  }
667:                  sb.addNV(ns, quote(value));
668:                 }
669:          } else if (v instanceof LiteralValue) {
670:                 value = ((LiteralValue)v).value;
671:                 sb.addNV(name, quote(value));
672:          } else if (v instanceof Value) {
673:                 /*
674:                  * XXX - We could split the encoded value into multiple
675:                  * segments if it's too long, but that's more difficult.
676:                  */
677:                 name += "*";
678:                 value = ((Value)v).encodedValue;
679:                 sb.addNV(name, quote(value));
680:          } else {
681:                 value = (String)v;
682:                 /*
683:                  * If this value is "long", split it into a multi-segment
684:                  * parameter. Only do this if we've enabled RFC2231 style
685:                  * encoded parameters.
686:                  *
687:                  * Note that we check the length before quoting the value.
688:                  * Quoting might make the string longer, although typically
689:                  * not much, so we allow a little slop in the calculation.
690:                  * In the worst case, a 60 character string will turn into
691:                  * 122 characters when quoted, which is long but not
692:                  * outrageous.
693:                  */
694:                 if (value.length() > 60 &&
695:                                 splitLongParameters && encodeParameters) {
696:                  int seg = 0;
697:                  name += "*";
698:                  while (value.length() > 60) {
699:                         sb.addNV(name + seg, quote(value.substring(0, 60)));
700:                         value = value.substring(60);
701:                         seg++;
702:                  }
703:                  if (value.length() > 0)
704:                         sb.addNV(name + seg, quote(value));
705:                 } else {
706:                  sb.addNV(name, quote(value));
707:                 }
708:          }
709: }
710: return sb.toString();
711: }
712:
713: /**
714: * A special wrapper for a StringBuffer that keeps track of the
715: * number of characters used in a line, wrapping to a new line
716: * as necessary; for use by the toString method.
717: */
718: private static class ToStringBuffer {
719:         private int used;        // keep track of how much used on current line
720:         private StringBuilder sb = new StringBuilder();
721:
722:         public ToStringBuffer(int used) {
723:          this.used = used;
724:         }
725:
726:         public void addNV(String name, String value) {
727:          sb.append("; ");
728:          used += 2;
729:          int len = name.length() + value.length() + 1;
730:          if (used + len > 76) { // overflows ...
731:                 sb.append("\r\n\t"); // .. start new continuation line
732:                 used = 8; // account for the starting <tab> char
733:          }
734:          sb.append(name).append('=');
735:          used += name.length() + 1;
736:          if (used + value.length() > 76) { // still overflows ...
737:                 // have to fold value
738:                 String s = MimeUtility.fold(used, value);
739:                 sb.append(s);
740:                 int lastlf = s.lastIndexOf('\n');
741:                 if (lastlf >= 0)        // always true
742:                  used += s.length() - lastlf - 1;
743:                 else
744:                  used += s.length();
745:          } else {
746:                 sb.append(value);
747:                 used += value.length();
748:          }
749:         }
750:
751:         @Override
752:         public String toString() {
753:          return sb.toString();
754:         }
755: }
756:
757: // Quote a parameter value token if required.
758: private static String quote(String value) {
759:         return MimeUtility.quote(value, HeaderTokenizer.MIME);
760: }
761:
762: private static final char hex[] = {
763:         '0','1', '2', '3', '4', '5', '6', '7',
764:         '8','9', 'A', 'B', 'C', 'D', 'E', 'F'
765: };
766:
767: /**
768: * Encode a parameter value, if necessary.
769: * If the value is encoded, a Value object is returned.
770: * Otherwise, null is returned.
771: * XXX - Could return a MultiValue object if parameter value is too long.
772: */
773: private static Value encodeValue(String value, String charset) {
774:         if (MimeUtility.checkAscii(value) == MimeUtility.ALL_ASCII)
775:          return null;        // no need to encode it
776:
777:         byte[] b;        // charset encoded bytes from the string
778:         try {
779:          b = value.getBytes(MimeUtility.javaCharset(charset));
780:         } catch (UnsupportedEncodingException ex) {
781:          return null;
782:         }
783:         StringBuffer sb = new StringBuffer(b.length + charset.length() + 2);
784:         sb.append(charset).append("''");
785:         for (int i = 0; i < b.length; i++) {
786:          char c = (char)(b[i] & 0xff);
787:          // do we need to encode this character?
788:          if (c <= ' ' || c >= 0x7f || c == '*' || c == '\'' || c == '%' ||
789:                  HeaderTokenizer.MIME.indexOf(c) >= 0) {
790:                 sb.append('%').append(hex[c>>4]).append(hex[c&0xf]);
791:          } else
792:                 sb.append(c);
793:         }
794:         Value v = new Value();
795:         v.charset = charset;
796:         v.value = value;
797:         v.encodedValue = sb.toString();
798:         return v;
799: }
800:
801: /**
802: * Extract charset and encoded value.
803: * Value will be decoded later.
804: */
805: private static Value extractCharset(String value) throws ParseException {
806:         Value v = new Value();
807:         v.value = v.encodedValue = value;
808:         try {
809:          int i = value.indexOf('\'');
810:          if (i < 0) {
811:                 if (decodeParametersStrict)
812:                  throw new ParseException(
813:                         "Missing charset in encoded value: " + value);
814:                 return v;        // not encoded correctly? return as is.
815:          }
816:          String charset = value.substring(0, i);
817:          int li = value.indexOf('\'', i + 1);
818:          if (li < 0) {
819:                 if (decodeParametersStrict)
820:                  throw new ParseException(
821:                         "Missing language in encoded value: " + value);
822:                 return v;        // not encoded correctly? return as is.
823:          }
824:          // String lang = value.substring(i + 1, li);
825:          v.value = value.substring(li + 1);
826:          v.charset = charset;
827:         } catch (NumberFormatException nex) {
828:          if (decodeParametersStrict)
829:                 throw new ParseException(nex.toString());
830:         } catch (StringIndexOutOfBoundsException ex) {
831:          if (decodeParametersStrict)
832:                 throw new ParseException(ex.toString());
833:         }
834:         return v;
835: }
836:
837: /**
838: * Decode the encoded bytes in value using the specified charset.
839: */
840: private static String decodeBytes(String value, String charset)
841:                         throws ParseException, UnsupportedEncodingException {
842:         /*
843:          * Decode the ASCII characters in value
844:          * into an array of bytes, and then convert
845:          * the bytes to a String using the specified
846:          * charset. We'll never need more bytes than
847:          * encoded characters, so use that to size the
848:          * array.
849:          */
850:         byte[] b = new byte[value.length()];
851:         int i, bi;
852:         for (i = 0, bi = 0; i < value.length(); i++) {
853:          char c = value.charAt(i);
854:          if (c == '%') {
855:                 try {
856:                  String hex = value.substring(i + 1, i + 3);
857:                  c = (char)Integer.parseInt(hex, 16);
858:                  i += 2;
859:                 } catch (NumberFormatException ex) {
860:                  if (decodeParametersStrict)
861:                         throw new ParseException(ex.toString());
862:                 } catch (StringIndexOutOfBoundsException ex) {
863:                  if (decodeParametersStrict)
864:                         throw new ParseException(ex.toString());
865:                 }
866:          }
867:          b[bi++] = (byte)c;
868:         }
869:         if (charset != null)
870:          charset = MimeUtility.javaCharset(charset);
871:         if (charset == null || charset.length() == 0)
872:          charset = MimeUtility.getDefaultJavaCharset();
873:         return new String(b, 0, bi, charset);
874: }
875:
876: /**
877: * Decode the encoded bytes in value and write them to the OutputStream.
878: */
879: private static void decodeBytes(String value, OutputStream os)
880:                                 throws ParseException, IOException {
881:         /*
882:          * Decode the ASCII characters in value
883:          * and write them to the stream.
884:          */
885:         int i;
886:         for (i = 0; i < value.length(); i++) {
887:          char c = value.charAt(i);
888:          if (c == '%') {
889:                 try {
890:                  String hex = value.substring(i + 1, i + 3);
891:                  c = (char)Integer.parseInt(hex, 16);
892:                  i += 2;
893:                 } catch (NumberFormatException ex) {
894:                  if (decodeParametersStrict)
895:                         throw new ParseException(ex.toString());
896:                 } catch (StringIndexOutOfBoundsException ex) {
897:                  if (decodeParametersStrict)
898:                         throw new ParseException(ex.toString());
899:                 }
900:          }
901:          os.write((byte)c);
902:         }
903: }
904: }