Skip to content

Package: ParameterList$MultiValue

ParameterList$MultiValue

Coverage

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