Return-Path: Delivered-To: apmail-geronimo-scm-archive@www.apache.org Received: (qmail 26505 invoked from network); 14 Jul 2006 10:04:12 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (209.237.227.199) by minotaur.apache.org with SMTP; 14 Jul 2006 10:04:12 -0000 Received: (qmail 13894 invoked by uid 500); 14 Jul 2006 10:04:12 -0000 Delivered-To: apmail-geronimo-scm-archive@geronimo.apache.org Received: (qmail 13725 invoked by uid 500); 14 Jul 2006 10:04:11 -0000 Mailing-List: contact scm-help@geronimo.apache.org; run by ezmlm Precedence: bulk list-help: list-unsubscribe: List-Post: Reply-To: dev@geronimo.apache.org List-Id: Delivered-To: mailing list scm@geronimo.apache.org Received: (qmail 13681 invoked by uid 99); 14 Jul 2006 10:04:11 -0000 Received: from asf.osuosl.org (HELO asf.osuosl.org) (140.211.166.49) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 14 Jul 2006 03:04:10 -0700 X-ASF-Spam-Status: No, hits=-9.4 required=10.0 tests=ALL_TRUSTED,NO_REAL_NAME X-Spam-Check-By: apache.org Received-SPF: pass (asf.osuosl.org: local policy) Received: from [140.211.166.113] (HELO eris.apache.org) (140.211.166.113) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 14 Jul 2006 03:03:57 -0700 Received: by eris.apache.org (Postfix, from userid 65534) id 927351A981C; Fri, 14 Jul 2006 03:03:12 -0700 (PDT) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r421852 [8/15] - in /geronimo/specs/trunk: ./ geronimo-spec-j2ee/ geronimo-spec-javamail-1.3.1/ geronimo-spec-javamail-1.3.1/src/ geronimo-spec-javamail-1.4/ geronimo-spec-javamail-1.4/src/ geronimo-spec-javamail-1.4/src/main/ geronimo-spec... Date: Fri, 14 Jul 2006 10:02:29 -0000 To: scm@geronimo.apache.org From: rickmcguire@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20060714100312.927351A981C@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org X-Spam-Rating: minotaur.apache.org 1.6.2 0/1000/N Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/MimeUtility.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/MimeUtility.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/MimeUtility.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/MimeUtility.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,1275 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.internet; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +import javax.activation.DataHandler; +import javax.activation.DataSource; +import javax.mail.MessagingException; + +import org.apache.geronimo.mail.util.ASCIIUtil; +import org.apache.geronimo.mail.util.Base64; +import org.apache.geronimo.mail.util.Base64DecoderStream; +import org.apache.geronimo.mail.util.Base64Encoder; +import org.apache.geronimo.mail.util.Base64EncoderStream; +import org.apache.geronimo.mail.util.QuotedPrintableDecoderStream; +import org.apache.geronimo.mail.util.QuotedPrintableEncoderStream; +import org.apache.geronimo.mail.util.QuotedPrintableEncoder; +import org.apache.geronimo.mail.util.QuotedPrintable; +import org.apache.geronimo.mail.util.SessionUtil; +import org.apache.geronimo.mail.util.UUDecoderStream; +import org.apache.geronimo.mail.util.UUEncoderStream; + +// encodings include "base64", "quoted-printable", "7bit", "8bit" and "binary". +// In addition, "uuencode" is also supported. The + +/** + * @version $Rev$ $Date$ + */ +public class MimeUtility { + + private static final String MIME_FOLDENCODEDWORDS = "mail.mime.foldencodedwords"; + private static final String MIME_DECODE_TEXT_STRICT = "mail.mime.decodetext.strict"; + private static final String MIME_FOLDTEXT = "mail.mime.foldtext"; + private static final int FOLD_THRESHOLD = 76; + + private MimeUtility() { + } + + public static final int ALL = -1; + + private static String defaultJavaCharset; + private static String escapedChars = "\"\\\r\n"; + private static String linearWhiteSpace = " \t\r\n"; + + private static String QP_WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~"; + private static String QP_TEXT_SPECIALS = "=_?"; + + // the javamail spec includes the ability to map java encoding names to MIME-specified names. Normally, + // these values are loaded from a character mapping file. + private static Map java2mime; + private static Map mime2java; + + static { + // we need to load the mapping tables used by javaCharset() and mimeCharset(). + loadCharacterSetMappings(); + } + + public static InputStream decode(InputStream in, String encoding) throws MessagingException { + encoding = encoding.toLowerCase(); + + // some encodies are just pass-throughs, with no real decoding. + if (encoding.equals("binary") || encoding.equals("7bit") || encoding.equals("8bit")) { + return in; + } + else if (encoding.equals("base64")) { + return new Base64DecoderStream(in); + } + // UUEncode is known by a couple historical extension names too. + else if (encoding.equals("uuencode") || encoding.equals("x-uuencode") || encoding.equals("x-uue")) { + return new UUDecoderStream(in); + } + else if (encoding.equals("quoted-printable")) { + return new QuotedPrintableDecoderStream(in); + } + else { + throw new MessagingException("Unknown encoding " + encoding); + } + } + + /** + * Decode a string of text obtained from a mail header into + * it's proper form. The text generally will consist of a + * string of tokens, some of which may be encoded using + * base64 encoding. + * + * @param text The text to decode. + * + * @return The decoded test string. + * @exception UnsupportedEncodingException + */ + public static String decodeText(String text) throws UnsupportedEncodingException { + // if the text contains any encoded tokens, those tokens will be marked with "=?". If the + // source string doesn't contain that sequent, no decoding is required. + if (text.indexOf("=?") < 0) { + return text; + } + + // we have two sets of rules we can apply. + if (!SessionUtil.getBooleanProperty(MIME_DECODE_TEXT_STRICT, true)) { + return decodeTextNonStrict(text); + } + + int offset = 0; + int endOffset = text.length(); + + int startWhiteSpace = -1; + int endWhiteSpace = -1; + + StringBuffer decodedText = new StringBuffer(text.length()); + + boolean previousTokenEncoded = false; + + while (offset < endOffset) { + char ch = text.charAt(offset); + + // is this a whitespace character? + if (linearWhiteSpace.indexOf(ch) != -1) { + startWhiteSpace = offset; + while (offset < endOffset) { + // step over the white space characters. + ch = text.charAt(offset); + if (linearWhiteSpace.indexOf(ch) != -1) { + offset++; + } + else { + // record the location of the first non lwsp and drop down to process the + // token characters. + endWhiteSpace = offset; + break; + } + } + } + else { + // we have a word token. We need to scan over the word and then try to parse it. + int wordStart = offset; + + while (offset < endOffset) { + // step over the white space characters. + ch = text.charAt(offset); + if (linearWhiteSpace.indexOf(ch) == -1) { + offset++; + } + else { + break; + } + + //NB: Trailing whitespace on these header strings will just be discarded. + } + // pull out the word token. + String word = text.substring(wordStart, offset); + // is the token encoded? decode the word + if (word.startsWith("=?")) { + try { + // if this gives a parsing failure, treat it like a non-encoded word. + String decodedWord = decodeWord(word); + + // are any whitespace characters significant? Append 'em if we've got 'em. + if (!previousTokenEncoded) { + if (startWhiteSpace != -1) { + decodedText.append(text.substring(startWhiteSpace, endWhiteSpace)); + startWhiteSpace = -1; + } + } + // this is definitely a decoded token. + previousTokenEncoded = true; + // and add this to the text. + decodedText.append(decodedWord); + // we continue parsing from here...we allow parsing errors to fall through + // and get handled as normal text. + continue; + + } catch (ParseException e) { + } + } + // this is a normal token, so it doesn't matter what the previous token was. Add the white space + // if we have it. + if (startWhiteSpace != -1) { + decodedText.append(text.substring(startWhiteSpace, endWhiteSpace)); + startWhiteSpace = -1; + } + // this is not a decoded token. + previousTokenEncoded = false; + decodedText.append(word); + } + } + + return decodedText.toString(); + } + + + /** + * Decode a string of text obtained from a mail header into + * it's proper form. The text generally will consist of a + * string of tokens, some of which may be encoded using + * base64 encoding. This is for non-strict decoded for mailers that + * violate the RFC 2047 restriction that decoded tokens must be delimited + * by linear white space. This will scan tokens looking for inner tokens + * enclosed in "=?" -- "?=" pairs. + * + * @param text The text to decode. + * + * @return The decoded test string. + * @exception UnsupportedEncodingException + */ + private static String decodeTextNonStrict(String text) throws UnsupportedEncodingException { + int offset = 0; + int endOffset = text.length(); + + int startWhiteSpace = -1; + int endWhiteSpace = -1; + + StringBuffer decodedText = new StringBuffer(text.length()); + + boolean previousTokenEncoded = false; + + while (offset < endOffset) { + char ch = text.charAt(offset); + + // is this a whitespace character? + if (linearWhiteSpace.indexOf(ch) != -1) { + startWhiteSpace = offset; + while (offset < endOffset) { + // step over the white space characters. + ch = text.charAt(offset); + if (linearWhiteSpace.indexOf(ch) != -1) { + offset++; + } + else { + // record the location of the first non lwsp and drop down to process the + // token characters. + endWhiteSpace = offset; + break; + } + } + } + else { + // we're at the start of a word token. We potentially need to break this up into subtokens + int wordStart = offset; + + while (offset < endOffset) { + // step over the white space characters. + ch = text.charAt(offset); + if (linearWhiteSpace.indexOf(ch) == -1) { + offset++; + } + else { + break; + } + + //NB: Trailing whitespace on these header strings will just be discarded. + } + // pull out the word token. + String word = text.substring(wordStart, offset); + + int decodeStart = 0; + + // now scan and process each of the bits within here. + while (decodeStart < word.length()) { + int tokenStart = word.indexOf("=?", decodeStart); + if (tokenStart == -1) { + // this is a normal token, so it doesn't matter what the previous token was. Add the white space + // if we have it. + if (startWhiteSpace != -1) { + decodedText.append(text.substring(startWhiteSpace, endWhiteSpace)); + startWhiteSpace = -1; + } + // this is not a decoded token. + previousTokenEncoded = false; + decodedText.append(word.substring(decodeStart)); + // we're finished. + break; + } + // we have something to process + else { + // we might have a normal token preceeding this. + if (tokenStart != decodeStart) { + // this is a normal token, so it doesn't matter what the previous token was. Add the white space + // if we have it. + if (startWhiteSpace != -1) { + decodedText.append(text.substring(startWhiteSpace, endWhiteSpace)); + startWhiteSpace = -1; + } + // this is not a decoded token. + previousTokenEncoded = false; + decodedText.append(word.substring(decodeStart, tokenStart)); + } + + // now find the end marker. + int tokenEnd = word.indexOf("?=", tokenStart); + // sigh, an invalid token. Treat this as plain text. + if (tokenEnd == -1) { + // this is a normal token, so it doesn't matter what the previous token was. Add the white space + // if we have it. + if (startWhiteSpace != -1) { + decodedText.append(text.substring(startWhiteSpace, endWhiteSpace)); + startWhiteSpace = -1; + } + // this is not a decoded token. + previousTokenEncoded = false; + decodedText.append(word.substring(tokenStart)); + // we're finished. + break; + } + else { + // update our ticker + decodeStart = tokenEnd + 2; + + String token = word.substring(tokenStart, tokenEnd); + try { + // if this gives a parsing failure, treat it like a non-encoded word. + String decodedWord = decodeWord(token); + + // are any whitespace characters significant? Append 'em if we've got 'em. + if (!previousTokenEncoded) { + if (startWhiteSpace != -1) { + decodedText.append(text.substring(startWhiteSpace, endWhiteSpace)); + startWhiteSpace = -1; + } + } + // this is definitely a decoded token. + previousTokenEncoded = true; + // and add this to the text. + decodedText.append(decodedWord); + // we continue parsing from here...we allow parsing errors to fall through + // and get handled as normal text. + continue; + + } catch (ParseException e) { + } + // this is a normal token, so it doesn't matter what the previous token was. Add the white space + // if we have it. + if (startWhiteSpace != -1) { + decodedText.append(text.substring(startWhiteSpace, endWhiteSpace)); + startWhiteSpace = -1; + } + // this is not a decoded token. + previousTokenEncoded = false; + decodedText.append(token); + } + } + } + } + } + + return decodedText.toString(); + } + + /** + * Parse a string using the RFC 2047 rules for an "encoded-word" + * type. This encoding has the syntax: + * + * encoded-word = "=?" charset "?" encoding "?" encoded-text "?=" + * + * @param word The possibly encoded word value. + * + * @return The decoded word. + * @exception ParseException + * @exception UnsupportedEncodingException + */ + public static String decodeWord(String word) throws ParseException, UnsupportedEncodingException { + // encoded words start with the characters "=?". If this not an encoded word, we throw a + // ParseException for the caller. + + if (!word.startsWith("=?")) { + throw new ParseException("Invalid RFC 2047 encoded-word: " + word); + } + + int charsetPos = word.indexOf('?', 2); + if (charsetPos == -1) { + throw new ParseException("Missing charset in RFC 2047 encoded-word: " + word); + } + + // pull out the character set information (this is the MIME name at this point). + String charset = word.substring(2, charsetPos).toLowerCase(); + + // now pull out the encoding token the same way. + int encodingPos = word.indexOf('?', charsetPos + 1); + if (encodingPos == -1) { + throw new ParseException("Missing encoding in RFC 2047 encoded-word: " + word); + } + + String encoding = word.substring(charsetPos + 1, encodingPos); + + // and finally the encoded text. + int encodedTextPos = word.indexOf("?=", encodingPos + 1); + if (encodedTextPos == -1) { + throw new ParseException("Missing encoded text in RFC 2047 encoded-word: " + word); + } + + String encodedText = word.substring(encodingPos + 1, encodedTextPos); + + // seems a bit silly to encode a null string, but easy to deal with. + if (encodedText.length() == 0) { + return ""; + } + + try { + // the decoder writes directly to an output stream. + ByteArrayOutputStream out = new ByteArrayOutputStream(encodedText.length()); + + byte[] encodedData = encodedText.getBytes("US-ASCII"); + + // Base64 encoded? + if (encoding.equals("B")) { + Base64.decode(encodedData, out); + } + // maybe quoted printable. + else if (encoding.equals("Q")) { + QuotedPrintableEncoder dataEncoder = new QuotedPrintableEncoder(); + dataEncoder.decodeWord(encodedData, out); + } + else { + throw new UnsupportedEncodingException("Unknown RFC 2047 encoding: " + encoding); + } + // get the decoded byte data and convert into a string. + byte[] decodedData = out.toByteArray(); + return new String(decodedData, javaCharset(charset)); + } catch (IOException e) { + throw new UnsupportedEncodingException("Invalid RFC 2047 encoding"); + } + + } + + /** + * Wrap an encoder around a given output stream. + * + * @param out The output stream to wrap. + * @param encoding The name of the encoding. + * + * @return A instance of FilterOutputStream that manages on the fly + * encoding for the requested encoding type. + * @exception MessagingException + */ + public static OutputStream encode(OutputStream out, String encoding) throws MessagingException { + // no encoding specified, so assume it goes out unchanged. + if (encoding == null) { + return out; + } + + encoding = encoding.toLowerCase(); + + // some encodies are just pass-throughs, with no real decoding. + if (encoding.equals("binary") || encoding.equals("7bit") || encoding.equals("8bit")) { + return out; + } + else if (encoding.equals("base64")) { + return new Base64EncoderStream(out); + } + // UUEncode is known by a couple historical extension names too. + else if (encoding.equals("uuencode") || encoding.equals("x-uuencode") || encoding.equals("x-uue")) { + return new UUEncoderStream(out); + } + else if (encoding.equals("quoted-printable")) { + return new QuotedPrintableEncoderStream(out); + } + else { + throw new MessagingException("Unknown encoding " + encoding); + } + } + + /** + * Wrap an encoder around a given output stream. + * + * @param out The output stream to wrap. + * @param encoding The name of the encoding. + * @param filename The filename of the data being sent (only used for UUEncode). + * + * @return A instance of FilterOutputStream that manages on the fly + * encoding for the requested encoding type. + * @exception MessagingException + */ + public static OutputStream encode(OutputStream out, String encoding, String filename) throws MessagingException { + encoding = encoding.toLowerCase(); + + // some encodies are just pass-throughs, with no real decoding. + if (encoding.equals("binary") || encoding.equals("7bit") || encoding.equals("8bit")) { + return out; + } + else if (encoding.equals("base64")) { + return new Base64EncoderStream(out); + } + // UUEncode is known by a couple historical extension names too. + else if (encoding.equals("uuencode") || encoding.equals("x-uuencode") || encoding.equals("x-uue")) { + return new UUEncoderStream(out, filename); + } + else if (encoding.equals("quoted-printable")) { + return new QuotedPrintableEncoderStream(out); + } + else { + throw new MessagingException("Unknown encoding " + encoding); + } + } + + + public static String encodeText(String word) throws UnsupportedEncodingException { + return encodeText(word, null, null); + } + + public static String encodeText(String word, String charset, String encoding) throws UnsupportedEncodingException { + return encodeWord(word, charset, encoding, false); + } + + public static String encodeWord(String word) throws UnsupportedEncodingException { + return encodeWord(word, null, null); + } + + public static String encodeWord(String word, String charset, String encoding) throws UnsupportedEncodingException { + return encodeWord(word, charset, encoding, true); + } + + + private static String encodeWord(String word, String charset, String encoding, boolean encodingWord) throws UnsupportedEncodingException { + + // figure out what we need to encode this. + String encoder = ASCIIUtil.getTextTransferEncoding(word); + // all ascii? We can return this directly, + if (encoder.equals("7bit")) { + return word; + } + + // if not given a charset, use the default. + if (charset == null) { + charset = getDefaultMIMECharset(); + } + + // sort out the encoder. If not explicitly given, use the best guess we've already established. + if (encoding != null) { + if (encoding.equalsIgnoreCase("B")) { + encoder = "base64"; + } + else if (encoding.equalsIgnoreCase("Q")) { + encoder = "quoted-printable"; + } + else { + throw new UnsupportedEncodingException("Unknown transfer encoding: " + encoding); + } + } + + try { + // get the string bytes in the correct source charset + InputStream in = new ByteArrayInputStream(word.getBytes( javaCharset(charset))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + if (encoder.equals("base64")) { + Base64Encoder dataEncoder = new Base64Encoder(); + dataEncoder.encodeWord(in, charset, out, SessionUtil.getBooleanProperty(MIME_FOLDENCODEDWORDS, false)); + } + else { + QuotedPrintableEncoder dataEncoder = new QuotedPrintableEncoder(); + dataEncoder.encodeWord(in, charset, encodingWord ? QP_WORD_SPECIALS : QP_TEXT_SPECIALS, out, SessionUtil.getBooleanProperty(MIME_FOLDENCODEDWORDS, false)); + } + + byte[] bytes = out.toByteArray(); + return new String(bytes); + } catch (IOException e) { + throw new UnsupportedEncodingException("Invalid encoding"); + } + } + + + /** + * Examine the content of a data source and decide what type + * of transfer encoding should be used. For text streams, + * we'll decided between 7bit, quoted-printable, and base64. + * For binary content types, we'll use either 7bit or base64. + * + * @param handler The DataHandler associated with the content. + * + * @return The string name of an encoding used to transfer the content. + */ + public static String getEncoding(DataHandler handler) { + + + // if this handler has an associated data source, we can read directly from the + // data source to make this judgment. This is generally MUCH faster than asking the + // DataHandler to write out the data for us. + DataSource ds = handler.getDataSource(); + if (ds != null) { + return getEncoding(ds); + } + + try { + // get a parser that allows us to make comparisons. + ContentType content = new ContentType(ds.getContentType()); + + // The only access to the content bytes at this point is by asking the handler to write + // the information out to a stream. We're going to pipe this through a special stream + // that examines the bytes as they go by. + ContentCheckingOutputStream checker = new ContentCheckingOutputStream(); + + handler.writeTo(checker); + + // figure this out based on whether we believe this to be a text type or not. + if (content.match("text/*")) { + return checker.getTextTransferEncoding(); + } + else { + return checker.getBinaryTransferEncoding(); + } + + } catch (Exception e) { + // any unexpected I/O exceptions we'll force to a "safe" fallback position. + return "base64"; + } + } + + + /** + * Determine the what transfer encoding should be used for + * data retrieved from a DataSource. + * + * @param source The DataSource for the transmitted data. + * + * @return The string name of the encoding form that should be used for + * the data. + */ + public static String getEncoding(DataSource source) { + InputStream in = null; + + try { + // get a parser that allows us to make comparisons. + ContentType content = new ContentType(source.getContentType()); + + // we're probably going to have to scan the data. + in = source.getInputStream(); + + if (!content.match("text/*")) { + // Not purporting to be a text type? Examine the content to see we might be able to + // at least pretend it is an ascii type. + return ASCIIUtil.getBinaryTransferEncoding(in); + } + else { + return ASCIIUtil.getTextTransferEncoding(in); + } + } catch (Exception e) { + // this was a problem...not sure what makes sense here, so we'll assume it's binary + // and we need to transfer this using Base64 encoding. + return "base64"; + } finally { + // make sure we close the stream + try { + if (in != null) { + in.close(); + } + } catch (IOException e) { + } + } + } + + + /** + * Quote a "word" value. If the word contains any character from + * the specified "specials" list, this value is returned as a + * quoted strong. Otherwise, it is returned unchanged (an "atom"). + * + * @param word The word requiring quoting. + * @param specials The set of special characters that can't appear in an unquoted + * string. + * + * @return The quoted value. This will be unchanged if the word doesn't contain + * any of the designated special characters. + */ + public static String quote(String word, String specials) { + int wordLength = word.length(); + boolean requiresQuoting = false; + // scan the string looking for problem characters + for (int i =0; i < wordLength; i++) { + char ch = word.charAt(i); + // special escaped characters require escaping, which also implies quoting. + if (escapedChars.indexOf(ch) >= 0) { + return quoteAndEscapeString(word); + } + // now check for control characters or the designated special characters. + if (ch < 32 || ch >= 127 || specials.indexOf(ch) >= 0) { + // we know this requires quoting, but we still need to scan the entire string to + // see if contains chars that require escaping. Just go ahead and treat it as if it does. + return quoteAndEscapeString(word); + } + } + return word; + } + + /** + * Take a string and return it as a formatted quoted string, with + * all characters requiring escaping handled properly. + * + * @param word The string to quote. + * + * @return The quoted string. + */ + private static String quoteAndEscapeString(String word) { + int wordLength = word.length(); + // allocate at least enough for the string and two quotes plus a reasonable number of escaped chars. + StringBuffer buffer = new StringBuffer(wordLength + 10); + // add the leading quote. + buffer.append('"'); + + for (int i = 0; i < wordLength; i++) { + char ch = word.charAt(i); + // is this an escaped char? + if (escapedChars.indexOf(ch) >= 0) { + // add the escape marker before appending. + buffer.append('\\'); + } + buffer.append(ch); + } + // now the closing quote + buffer.append('"'); + return buffer.toString(); + } + + /** + * Translate a MIME standard character set name into the Java + * equivalent. + * + * @param charset The MIME standard name. + * + * @return The Java equivalent for this name. + */ + public static String javaCharset(String charset) { + // nothing in, nothing out. + if (charset == null) { + return null; + } + + String mappedCharset = (String)mime2java.get(charset.toLowerCase()); + // if there is no mapping, then the original name is used. Many of the MIME character set + // names map directly back into Java. The reverse isn't necessarily true. + return mappedCharset == null ? charset : mappedCharset; + } + + /** + * Map a Java character set name into the MIME equivalent. + * + * @param charset The java character set name. + * + * @return The MIME standard equivalent for this character set name. + */ + public static String mimeCharset(String charset) { + // nothing in, nothing out. + if (charset == null) { + return null; + } + + String mappedCharset = (String)java2mime.get(charset.toLowerCase()); + // if there is no mapping, then the original name is used. Many of the MIME character set + // names map directly back into Java. The reverse isn't necessarily true. + return mappedCharset == null ? charset : mappedCharset; + } + + + /** + * Get the default character set to use, in Java name format. + * This either be the value set with the mail.mime.charset + * system property or obtained from the file.encoding system + * property. If neither of these is set, we fall back to + * 8859_1 (basically US-ASCII). + * + * @return The character string value of the default character set. + */ + public static String getDefaultJavaCharset() { + String charset = SessionUtil.getProperty("mail.mime.charset"); + if (charset != null) { + return javaCharset(charset); + } + return SessionUtil.getProperty("file.encoding", "8859_1"); + } + + /** + * Get the default character set to use, in MIME name format. + * This either be the value set with the mail.mime.charset + * system property or obtained from the file.encoding system + * property. If neither of these is set, we fall back to + * 8859_1 (basically US-ASCII). + * + * @return The character string value of the default character set. + */ + static String getDefaultMIMECharset() { + // if the property is specified, this can be used directly. + String charset = SessionUtil.getProperty("mail.mime.charset"); + if (charset != null) { + return charset; + } + + // get the Java-defined default and map back to a MIME name. + return mimeCharset(SessionUtil.getProperty("file.encoding", "8859_1")); + } + + + /** + * Load the default mapping tables used by the javaCharset() + * and mimeCharset() methods. By default, these tables are + * loaded from the /META-INF/javamail.charset.map file. If + * something goes wrong loading that file, we configure things + * with a default mapping table (which just happens to mimic + * what's in the default mapping file). + */ + static private void loadCharacterSetMappings() { + java2mime = new HashMap(); + mime2java = new HashMap(); + + + // normally, these come from a character map file contained in the jar file. + try { + InputStream map = javax.mail.internet.MimeUtility.class.getResourceAsStream("/META-INF/javamail.charset.map"); + + if (map != null) { + // get a reader for this so we can load. + BufferedReader reader = new BufferedReader(new InputStreamReader(map)); + + readMappings(reader, java2mime); + readMappings(reader, mime2java); + } + } catch (Exception e) { + } + + // if any sort of error occurred reading the preferred file version, we could end up with empty + // mapping tables. This could cause all sorts of difficulty, so ensure they are populated with at + // least a reasonable set of defaults. + + // these mappings echo what's in the default file. + if (java2mime.isEmpty()) { + java2mime.put("8859_1", "ISO-8859-1"); + java2mime.put("iso8859_1", "ISO-8859-1"); + java2mime.put("iso8859-1", "ISO-8859-1"); + + java2mime.put("8859_2", "ISO-8859-2"); + java2mime.put("iso8859_2", "ISO-8859-2"); + java2mime.put("iso8859-2", "ISO-8859-2"); + + java2mime.put("8859_3", "ISO-8859-3"); + java2mime.put("iso8859_3", "ISO-8859-3"); + java2mime.put("iso8859-3", "ISO-8859-3"); + + java2mime.put("8859_4", "ISO-8859-4"); + java2mime.put("iso8859_4", "ISO-8859-4"); + java2mime.put("iso8859-4", "ISO-8859-4"); + + java2mime.put("8859_5", "ISO-8859-5"); + java2mime.put("iso8859_5", "ISO-8859-5"); + java2mime.put("iso8859-5", "ISO-8859-5"); + + java2mime.put ("8859_6", "ISO-8859-6"); + java2mime.put("iso8859_6", "ISO-8859-6"); + java2mime.put("iso8859-6", "ISO-8859-6"); + + java2mime.put("8859_7", "ISO-8859-7"); + java2mime.put("iso8859_7", "ISO-8859-7"); + java2mime.put("iso8859-7", "ISO-8859-7"); + + java2mime.put("8859_8", "ISO-8859-8"); + java2mime.put("iso8859_8", "ISO-8859-8"); + java2mime.put("iso8859-8", "ISO-8859-8"); + + java2mime.put("8859_9", "ISO-8859-9"); + java2mime.put("iso8859_9", "ISO-8859-9"); + java2mime.put("iso8859-9", "ISO-8859-9"); + + java2mime.put("sjis", "Shift_JIS"); + java2mime.put ("jis", "ISO-2022-JP"); + java2mime.put("iso2022jp", "ISO-2022-JP"); + java2mime.put("euc_jp", "euc-jp"); + java2mime.put("koi8_r", "koi8-r"); + java2mime.put("euc_cn", "euc-cn"); + java2mime.put("euc_tw", "euc-tw"); + java2mime.put("euc_kr", "euc-kr"); + } + + if (mime2java.isEmpty ()) { + mime2java.put("iso-2022-cn", "ISO2022CN"); + mime2java.put("iso-2022-kr", "ISO2022KR"); + mime2java.put("utf-8", "UTF8"); + mime2java.put("utf8", "UTF8"); + mime2java.put("ja_jp.iso2022-7", "ISO2022JP"); + mime2java.put("ja_jp.eucjp", "EUCJIS"); + mime2java.put ("euc-kr", "KSC5601"); + mime2java.put("euckr", "KSC5601"); + mime2java.put("us-ascii", "ISO-8859-1"); + mime2java.put("x-us-ascii", "ISO-8859-1"); + } + } + + + /** + * Read a section of a character map table and populate the + * target mapping table with the information. The table end + * is marked by a line starting with "--" and also ending with + * "--". Blank lines and comment lines (beginning with '#') are + * ignored. + * + * @param reader The source of the file information. + * @param table The mapping table used to store the information. + */ + static private void readMappings(BufferedReader reader, Map table) throws IOException { + // process lines to the EOF or the end of table marker. + while (true) { + String line = reader.readLine(); + // no line returned is an EOF + if (line == null) { + return; + } + + // trim so we're not messed up by trailing blanks + line = line.trim(); + + if (line.length() == 0 || line.startsWith("#")) { + continue; + } + + // stop processing if this is the end-of-table marker. + if (line.startsWith("--") && line.endsWith("--")) { + return; + } + + // we allow either blanks or tabs as token delimiters. + StringTokenizer tokenizer = new StringTokenizer(line, " \t"); + + try { + String from = tokenizer.nextToken().toLowerCase(); + String to = tokenizer.nextToken(); + + table.put(from, to); + } catch (NoSuchElementException e) { + // just ignore the line if invalid. + } + } + } + + + /** + * Perform RFC 2047 text folding on a string of text. + * + * @param used The amount of text already "used up" on this line. This is + * typically the length of a message header that this text + * get getting added to. + * @param s The text to fold. + * + * @return The input text, with linebreaks inserted at appropriate fold points. + */ + public static String fold(int used, String s) { + // if folding is disable, unfolding is also. Return the string unchanged. + if (!SessionUtil.getBooleanProperty(MIME_FOLDTEXT, true)) { + return s; + } + + int end; + + // now we need to strip off any trailing "whitespace", where whitespace is blanks, tabs, + // and line break characters. + for (end = s.length() - 1; end >= 0; end--) { + int ch = s.charAt(end); + if (ch != ' ' && ch != '\t' ) { + break; + } + } + + // did we actually find something to remove? Shorten the String to the trimmed length + if (end != s.length() - 1) { + s = s.substring(0, end + 1); + } + + // does the string as it exists now not require folding? We can just had that back right off. + if (s.length() + used <= FOLD_THRESHOLD) { + return s; + } + + // get a buffer for the length of the string, plus room for a few line breaks. + // these are soft line breaks, so we generally need more that just the line breaks (an escape + + // CR + LF + leading space on next line); + StringBuffer newString = new StringBuffer(s.length() + 8); + + + // now keep chopping this down until we've accomplished what we need. + while (used + s.length() > FOLD_THRESHOLD) { + int breakPoint = -1; + char breakChar = 0; + + // now scan for the next place where we can break. + for (int i = 0; i < s.length(); i++) { + // have we passed the fold limit? + if (used + i > FOLD_THRESHOLD) { + // if we've already seen a blank, then stop now. Otherwise + // we keep going until we hit a fold point. + if (breakPoint != -1) { + break; + } + } + char ch = s.charAt(i); + + // a white space character? + if (ch == ' ' || ch == '\t') { + // this might be a run of white space, so skip over those now. + breakPoint = i; + // we need to maintain the same character type after the inserted linebreak. + breakChar = ch; + i++; + while (i < s.length()) { + ch = s.charAt(i); + if (ch != ' ' && ch != '\t') { + break; + } + i++; + } + } + // found an embedded new line. Escape this so that the unfolding process preserves it. + else if (ch == '\n') { + newString.append('\\'); + newString.append('\n'); + } + else if (ch == '\r') { + newString.append('\\'); + newString.append('\n'); + i++; + // if this is a CRLF pair, add the second char also + if (i < s.length() && s.charAt(i) == '\n') { + newString.append('\r'); + } + } + + } + // no fold point found, we punt, append the remainder and leave. + if (breakPoint == -1) { + newString.append(s); + return newString.toString(); + } + newString.append(s.substring(0, breakPoint)); + newString.append("\r\n"); + newString.append(breakChar); + // chop the string + s = s.substring(breakPoint + 1); + // start again, and we've used the first char of the limit already with the whitespace char. + used = 1; + } + + // add on the remainder, and return + newString.append(s); + return newString.toString(); + } + + /** + * Unfold a folded string. The unfolding process will remove + * any line breaks that are not escaped and which are also followed + * by whitespace characters. + * + * @param s The folded string. + * + * @return A new string with unfolding rules applied. + */ + public static String unfold(String s) { + // if folding is disable, unfolding is also. Return the string unchanged. + if (!SessionUtil.getBooleanProperty(MIME_FOLDTEXT, true)) { + return s; + } + + // if there are no line break characters in the string, we can just return this. + if (s.indexOf('\n') < 0 && s.indexOf('\r') < 0) { + return s; + } + + // we need to scan and fix things up. + int length = s.length(); + + StringBuffer newString = new StringBuffer(length); + + // scan the entire string + for (int i = 0; i < length; i++) { + char ch = s.charAt(i); + + // we have a backslash. In folded strings, escape characters are only processed as such if + // they preceed line breaks. Otherwise, we leave it be. + if (ch == '\\') { + // escape at the very end? Just add the character. + if (i == length - 1) { + newString.append(ch); + } + else { + int nextChar = s.charAt(i + 1); + + // naked newline? Add the new line to the buffer, and skip the escape char. + if (nextChar == '\n') { + newString.append('\n'); + i++; + } + else if (nextChar == '\r') { + // just the CR left? Add it, removing the escape. + if (i == length - 2 || s.charAt(i + 2) != '\r') { + newString.append('\r'); + i++; + } + else { + // toss the escape, add both parts of the CRLF, and skip over two chars. + newString.append('\r'); + newString.append('\n'); + i += 2; + } + } + else { + // an escape for another purpose, just copy it over. + newString.append(ch); + } + } + } + // we have an unescaped line break + else if (ch == '\n' || ch == '\r') { + // remember the position in case we need to backtrack. + int lineBreak = i; + boolean CRLF = false; + + if (ch == '\r') { + // check to see if we need to step over this. + if (i < length - 1 && s.charAt(i + 1) == '\n') { + i++; + // flag the type so we know what we might need to preserve. + CRLF = true; + } + } + + // get a temp position scanner. + int scan = i + 1; + + // does a blank follow this new line? we need to scrap the new line and reduce the leading blanks + // down to a single blank. + if (scan < length && s.charAt(scan) == ' ') { + // add the character + newString.append(' '); + + // scan over the rest of the blanks + i = scan + 1; + while (i < length && s.charAt(i) == ' ') { + i++; + } + // we'll increment down below, so back up to the last blank as the current char. + i--; + } + else { + // we must keep this line break. Append the appropriate style. + if (CRLF) { + newString.append("\r\n"); + } + else { + newString.append(ch); + } + } + } + else { + // just a normal, ordinary character + newString.append(ch); + } + } + return newString.toString(); + } +} + + +/** + * Utility class for examining content information written out + * by a DataHandler object. This stream gathers statistics on + * the stream so it can make transfer encoding determinations. + */ +class ContentCheckingOutputStream extends OutputStream { + private int asciiChars = 0; + private int nonAsciiChars = 0; + private boolean containsLongLines = false; + private boolean containsMalformedEOL = false; + private int previousChar = 0; + private int span = 0; + + ContentCheckingOutputStream() { + } + + public void write(byte[] data) throws IOException { + write(data, 0, data.length); + } + + public void write(byte[] data, int offset, int length) throws IOException { + for (int i = 0; i < length; i++) { + write(data[offset + i]); + } + } + + public void write(int ch) { + // we found a linebreak. Reset the line length counters on either one. We don't + // really need to validate here. + if (ch == '\n' || ch == '\r') { + // we found a newline, this is only valid if the previous char was the '\r' + if (ch == '\n') { + // malformed linebreak? force this to base64 encoding. + if (previousChar != '\r') { + containsMalformedEOL = true; + } + } + // hit a line end, reset our line length counter + span = 0; + } + else { + span++; + // the text has long lines, we can't transfer this as unencoded text. + if (span > 998) { + containsLongLines = true; + } + + // non-ascii character, we have to transfer this in binary. + if (!ASCIIUtil.isAscii(ch)) { + nonAsciiChars++; + } + else { + asciiChars++; + } + } + previousChar = ch; + } + + + public String getBinaryTransferEncoding() { + if (nonAsciiChars != 0 || containsLongLines || containsMalformedEOL) { + return "base64"; + } + else { + return "7bit"; + } + } + + public String getTextTransferEncoding() { + // looking good so far, only valid chars here. + if (nonAsciiChars == 0) { + // does this contain long text lines? We need to use a Q-P encoding which will + // be only slightly longer, but handles folding the longer lines. + if (containsLongLines) { + return "quoted-printable"; + } + else { + // ideal! Easiest one to handle. + return "7bit"; + } + } + else { + // mostly characters requiring encoding? Base64 is our best bet. + if (nonAsciiChars > asciiChars) { + return "base64"; + } + else { + // Q-P encoding will use fewer bytes than the full Base64. + return "quoted-printable"; + } + } + } +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/MimeUtility.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/MimeUtility.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/MimeUtility.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/NewsAddress.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/NewsAddress.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/NewsAddress.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/NewsAddress.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,150 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.internet; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import javax.mail.Address; + +import sun.security.provider.Sun; + +/** + * A representation of an RFC1036 Internet newsgroup address. + * + * @version $Rev$ $Date$ + */ +public class NewsAddress extends Address { + /** + * The host for this newsgroup + */ + protected String host; + + /** + * The name of this newsgroup + */ + protected String newsgroup; + + public NewsAddress() { + } + + public NewsAddress(String newsgroup) { + this.newsgroup = newsgroup; + } + + public NewsAddress(String newsgroup, String host) { + this.newsgroup = newsgroup; + this.host = host; + } + + /** + * The type of this address; always "news". + * @return "news" + */ + public String getType() { + return "news"; + } + + public void setNewsgroup(String newsgroup) { + this.newsgroup = newsgroup; + } + + public String getNewsgroup() { + return newsgroup; + } + + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return host; + } + + public String toString() { + // Sun impl only appears to return the newsgroup name, no host. + return newsgroup; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof NewsAddress)) return false; + + final NewsAddress newsAddress = (NewsAddress) o; + + if (host != null ? !host.equals(newsAddress.host) : newsAddress.host != null) return false; + if (newsgroup != null ? !newsgroup.equals(newsAddress.newsgroup) : newsAddress.newsgroup != null) return false; + + return true; + } + + public int hashCode() { + int result; + result = (host != null ? host.toLowerCase().hashCode() : 0); + result = 29 * result + (newsgroup != null ? newsgroup.hashCode() : 0); + return result; + } + + /** + * Parse a comma-spearated list of addresses. + * + * @param addresses the list to parse + * @return the array of extracted addresses + * @throws AddressException if one of the addresses is invalid + */ + public static NewsAddress[] parse(String addresses) throws AddressException { + List result = new ArrayList(); + StringTokenizer tokenizer = new StringTokenizer(addresses, ","); + while (tokenizer.hasMoreTokens()) { + String address = tokenizer.nextToken().trim(); + int index = address.indexOf('@'); + if (index == -1) { + result.add(new NewsAddress(address)); + } else { + String newsgroup = address.substring(0, index).trim(); + String host = address.substring(index+1).trim(); + result.add(new NewsAddress(newsgroup, host)); + } + } + return (NewsAddress[]) result.toArray(new NewsAddress[result.size()]); + } + + /** + * Convert the supplied addresses to a comma-separated String. + * If addresses is null, returns null; if empty, returns an empty string. + * + * @param addresses the addresses to convert + * @return a comma-separated list of addresses + */ + public static String toString(Address[] addresses) { + if (addresses == null) { + return null; + } + if (addresses.length == 0) { + return ""; + } + + StringBuffer result = new StringBuffer(addresses.length * 32); + result.append(addresses[0]); + for (int i = 1; i < addresses.length; i++) { + result.append(',').append(addresses[i].toString()); + } + return result.toString(); + } +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/NewsAddress.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/NewsAddress.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/NewsAddress.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParameterList.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParameterList.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParameterList.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParameterList.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,300 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.internet; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList;// Represents lists in things like +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.geronimo.mail.util.ASCIIUtil; +import org.apache.geronimo.mail.util.RFC2231Encoder; +import org.apache.geronimo.mail.util.SessionUtil; + +// Content-Type: text/plain;charset=klingon +// +// The ;charset=klingon is the parameter list, may have more of them with ';' + +/** + * @version $Rev$ $Date$ + */ +public class ParameterList { + private static final String MIME_ENCODEPARAMETERS = "mail.mime.encodeparameters"; + private static final String MIME_DECODEPARAMETERS = "mail.mime.decodeparameters"; + private static final String MIME_DECODEPARAMETERS_STRICT = "mail.mime.decodeparameters.strict"; + + private static final int HEADER_SIZE_LIMIT = 76; + + private Map _parameters = new HashMap(); + + private boolean encodeParameters = false; + private boolean decodeParameters = false; + private boolean decodeParametersStrict = false; + + public ParameterList() { + // figure out how parameter handling is to be performed. + getInitialProperties(); + } + + public ParameterList(String list) throws ParseException { + // figure out how parameter handling is to be performed. + getInitialProperties(); + // get a token parser for the type information + HeaderTokenizer tokenizer = new HeaderTokenizer(list, HeaderTokenizer.MIME); + while (true) { + HeaderTokenizer.Token token = tokenizer.next(); + + switch (token.getType()) { + // the EOF token terminates parsing. + case HeaderTokenizer.Token.EOF: + return; + + // each new parameter is separated by a semicolon, including the first, which separates + // the parameters from the main part of the header. + case ';': + // the next token needs to be a parameter name + token = tokenizer.next(); + // allow a trailing semicolon on the parameters. + if (token.getType() == HeaderTokenizer.Token.EOF) { + return; + } + + if (token.getType() != HeaderTokenizer.Token.ATOM) { + throw new ParseException("Invalid parameter name: " + token.getValue()); + } + + // get the parameter name as a lower case version for better mapping. + String name = token.getValue().toLowerCase(); + + token = tokenizer.next(); + + // parameters are name=value, so we must have the "=" here. + if (token.getType() != '=') { + throw new ParseException("Missing '='"); + } + + // now the value, which may be an atom or a literal + token = tokenizer.next(); + + if (token.getType() != HeaderTokenizer.Token.ATOM && token.getType() != HeaderTokenizer.Token.QUOTEDSTRING) { + throw new ParseException("Invalid parameter value: " + token.getValue()); + } + + String value = token.getValue(); + String decodedValue = null; + + // we might have to do some additional decoding. A name that ends with "*" + // is marked as being encoded, so if requested, we decode the value. + if (decodeParameters && name.endsWith("*")) { + // the name needs to be pruned of the marker, and we need to decode the value. + name = name.substring(0, name.length() - 1); + // get a new decoder + RFC2231Encoder decoder = new RFC2231Encoder(HeaderTokenizer.MIME); + + try { + // decode the value + decodedValue = decoder.decode(value); + } catch (Exception e) { + // if we're doing things strictly, then raise a parsing exception for errors. + // otherwise, leave the value in its current state. + if (decodeParametersStrict) { + throw new ParseException("Invalid RFC2231 encoded parameter"); + } + } + _parameters.put(name, new ParameterValue(name, decodedValue, value)); + } + else { + _parameters.put(name, new ParameterValue(name, value)); + } + + break; + + default: + throw new ParseException("Missing ';'"); + + } + } + } + + /** + * Get the initial parameters that control parsing and values. + * These parameters are controlled by System properties. + */ + private void getInitialProperties() { + decodeParameters = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS, false); + decodeParametersStrict = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS_STRICT, false); + encodeParameters = SessionUtil.getBooleanProperty(MIME_ENCODEPARAMETERS, true); + } + + public int size() { + return _parameters.size(); + } + + public String get(String name) { + ParameterValue value = (ParameterValue)_parameters.get(name.toLowerCase()); + if (value != null) { + return value.value; + } + return null; + } + + public void set(String name, String value) { + name = name.toLowerCase(); + _parameters.put(name, new ParameterValue(name, value)); + } + + public void set(String name, String value, String charset) { + name = name.toLowerCase(); + // only encode if told to and this contains non-ASCII charactes. + if (encodeParameters && !ASCIIUtil.isAscii(value)) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + RFC2231Encoder encoder = new RFC2231Encoder(HeaderTokenizer.MIME); + + // extract the bytes using the given character set and encode + byte[] valueBytes = value.getBytes(MimeUtility.javaCharset(charset)); + + // the string format is charset''data + out.write(charset.getBytes()); + out.write('\''); + out.write('\''); + encoder.encode(valueBytes, 0, valueBytes.length, out); + + // default in case there is an exception + _parameters.put(name, new ParameterValue(name, value, new String(out.toByteArray()))); + return; + + } catch (Exception e) { + // just fall through and set the value directly if there is an error + } + } + // default in case there is an exception + _parameters.put(name, new ParameterValue(name, value)); + } + + public void remove(String name) { + _parameters.remove(name); + } + + public Enumeration getNames() { + return Collections.enumeration(_parameters.keySet()); + } + + public String toString() { + // we need to perform folding, but out starting point is 0. + return toString(0); + } + + public String toString(int used) { + StringBuffer stringValue = new StringBuffer(); + + Iterator values = _parameters.values().iterator(); + + while (values.hasNext()) { + ParameterValue parm = (ParameterValue)values.next(); + // get the values we're going to encode in here. + String name = parm.getEncodedName(); + String value = parm.toString(); + + // add the semicolon separator. We also add a blank so that folding/unfolding rules can be used. + stringValue.append("; "); + used += 2; + + // too big for the current header line? + if ((used + name.length() + value.length() + 1) > HEADER_SIZE_LIMIT) { + // and a CRLF-whitespace combo. + stringValue.append("\r\n "); + // reset the counter for a fresh line + used = 3; + } + // now add the keyword/value pair. + stringValue.append(name); + stringValue.append("="); + + used += name.length() + 1; + + // we're not out of the woods yet. It is possible that the keyword/value pair by itself might + // be too long for a single line. If that's the case, the we need to fold the value, if possible + if (used + value.length() > HEADER_SIZE_LIMIT) { + String foldedValue = MimeUtility.fold(used, value); + + stringValue.append(foldedValue); + + // now we need to sort out how much of the current line is in use. + int lastLineBreak = foldedValue.lastIndexOf('\n'); + + if (lastLineBreak != -1) { + used = foldedValue.length() - lastLineBreak + 1; + } + else { + used += foldedValue.length(); + } + } + else { + // no folding required, just append. + stringValue.append(value); + used += value.length(); + } + } + + return stringValue.toString(); + } + + + /** + * Utility class for representing parameter values in the list. + */ + class ParameterValue { + public String name; // the name of the parameter + public String value; // the original set value + public String encodedValue; // an encoded value, if encoding is requested. + + public ParameterValue(String name, String value) { + this.name = name; + this.value = value; + this.encodedValue = null; + } + + public ParameterValue(String name, String value, String encodedValue) { + this.name = name; + this.value = value; + this.encodedValue = encodedValue; + } + + public String toString() { + if (encodedValue != null) { + return MimeUtility.quote(encodedValue, HeaderTokenizer.MIME); + } + return MimeUtility.quote(value, HeaderTokenizer.MIME); + } + + public String getEncodedName() { + if (encodedValue != null) { + return name + "*"; + } + return name; + } + } +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParameterList.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParameterList.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParameterList.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParseException.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParseException.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParseException.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParseException.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,33 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.internet; + +import javax.mail.MessagingException; + +/** + * @version $Rev$ $Date$ + */ +public class ParseException extends MessagingException { + public ParseException() { + super(); + } + + public ParseException(String message) { + super(message); + } +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParseException.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParseException.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/ParseException.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,86 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.internet; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.mail.MessagingException; + +/** + * @version $Rev$ $Date$ + */ + + +public class PreencodedMimeBodyPart extends MimeBodyPart { + // the defined transfer encoding + private String transferEncoding; + + + /** + * Create a new body part with the specified MIME transfer encoding. + * + * @param encoding The content encoding. + */ + public PreencodedMimeBodyPart(String encoding) { + transferEncoding = encoding; + } + + + /** + * Retieve the defined encoding for this body part. + * + * @return + * @exception MessagingException + */ + public String getEncoding() throws MessagingException { + return transferEncoding; + } + + /** + * Write the body part content to the stream without applying + * and additional encodings. + * + * @param out The target output stream. + * + * @exception IOException + * @exception MessagingException + */ + public void writeTo(OutputStream out) throws IOException, MessagingException { + headers.writeTo(out, null); + // add the separater between the headers and the data portion. + out.write('\r'); + out.write('\n'); + // write this out without getting an encoding stream + getDataHandler().writeTo(out); + out.flush(); + } + + + /** + * Override of update headers to ensure the transfer encoding + * is forced to the correct type. + * + * @exception MessagingException + */ + protected void updateHeaders() throws MessagingException { + super.updateHeaders(); + setHeader("Content-Transfer-Encoding", transferEncoding); + } +} + Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/SharedInputStream.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/SharedInputStream.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/SharedInputStream.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/SharedInputStream.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,29 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.internet; + +import java.io.InputStream; + +/** + * @version $Rev$ $Date$ + */ +public interface SharedInputStream { + public abstract long getPosition(); + + public abstract InputStream newStream(long start, long end); +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/SharedInputStream.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/SharedInputStream.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/internet/SharedInputStream.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressStringTerm.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressStringTerm.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressStringTerm.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressStringTerm.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,46 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.search; + +import javax.mail.Address; + +/** + * A Term that compares two Addresses as Strings. + * + * @version $Rev$ $Date$ + */ +public abstract class AddressStringTerm extends StringTerm { + /** + * Constructor. + * @param pattern the pattern to be compared + */ + protected AddressStringTerm(String pattern) { + super(pattern); + } + + /** + * Tests if the patterm associated with this Term is a substring of + * the address in the supplied object. + * + * @param address + * @return + */ + protected boolean match(Address address) { + return match(address.toString()); + } +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressStringTerm.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressStringTerm.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressStringTerm.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressTerm.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressTerm.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressTerm.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressTerm.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,70 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.search; + +import javax.mail.Address; + +/** + * Term that compares two addresses. + * + * @version $Rev$ $Date$ + */ +public abstract class AddressTerm extends SearchTerm { + /** + * The address. + */ + protected Address address; + + /** + * Constructor taking the address for this term. + * @param address the address + */ + protected AddressTerm(Address address) { + this.address = address; + } + + /** + * Return the address of this term. + * + * @return the addre4ss + */ + public Address getAddress() { + return address; + } + + /** + * Match to the supplied address. + * + * @param address the address to match with + * @return true if the addresses match + */ + protected boolean match(Address address) { + return this.address.equals(address); + } + + public boolean equals(Object other) { + if (this == other) return true; + if (other instanceof AddressTerm == false) return false; + + return address.equals(((AddressTerm) other).address); + } + + public int hashCode() { + return address.hashCode(); + } +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressTerm.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressTerm.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AddressTerm.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AndTerm.java URL: http://svn.apache.org/viewvc/geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AndTerm.java?rev=421852&view=auto ============================================================================== --- geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AndTerm.java (added) +++ geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AndTerm.java Fri Jul 14 03:02:19 2006 @@ -0,0 +1,90 @@ +/** + * + * Copyright 2003-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package javax.mail.search; + +import java.util.Arrays; +import javax.mail.Message; + +/** + * Term that implements a logical AND across terms. + * + * @version $Rev$ $Date$ + */ +public final class AndTerm extends SearchTerm { + /** + * Terms to which the AND operator should be applied. + */ + protected SearchTerm[] terms; + + /** + * Constructor for performing a binary AND. + * + * @param a the first term + * @param b the second ter, + */ + public AndTerm(SearchTerm a, SearchTerm b) { + terms = new SearchTerm[]{a, b}; + } + + /** + * Constructor for performing and AND across an arbitraty number of terms. + * @param terms the terms to AND together + */ + public AndTerm(SearchTerm[] terms) { + this.terms = terms; + } + + /** + * Return the terms. + * @return the terms + */ + public SearchTerm[] getTerms() { + return terms; + } + + /** + * Match by applying the terms, in order, to the Message and performing an AND operation + * to the result. Comparision will stop immediately if one of the terms returns false. + * + * @param message the Message to apply the terms to + * @return true if all terms match + */ + public boolean match(Message message) { + for (int i = 0; i < terms.length; i++) { + SearchTerm term = terms[i]; + if (!term.match(message)) { + return false; + } + } + return true; + } + + public boolean equals(Object other) { + if (other == this) return true; + if (other instanceof AndTerm == false) return false; + return Arrays.equals(terms, ((AndTerm) other).terms); + } + + public int hashCode() { + int hash = 0; + for (int i = 0; i < terms.length; i++) { + hash = hash * 37 + terms[i].hashCode(); + } + return hash; + } +} Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AndTerm.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AndTerm.java ------------------------------------------------------------------------------ svn:keywords = Date Revision Propchange: geronimo/specs/trunk/geronimo-spec-javamail-1.4/src/main/java/javax/mail/search/AndTerm.java ------------------------------------------------------------------------------ svn:mime-type = text/plain