All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.upokecenter.mail.Message Maven / Gradle / Ivy

package com.upokecenter.mail;
/*
Written by Peter O. in 2014.
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
If you like this, you should donate to Peter O.
at: http://upokecenter.dreamhosters.com/articles/donate-now-2/
 */

import java.util.*;
import java.io.*;

import com.upokecenter.util.*;
import com.upokecenter.mail.transforms.*;
import com.upokecenter.text.*;

    /**
     * 

Represents an email message, and contains methods and properties for * accessing and modifying email message data. This class implements the * Internet Message Format (RFC 5322) and Multipurpose Internet Mail * Extensions (MIME; RFC 2045-2047, RFC 2049).

Thread * safety: This class is mutable; its properties can be changed. * None of its instance methods are designed to be thread safe. * Therefore, access to objects from this class must be synchronized if * multiple threads can access them at the same time.

The * following lists known deviations from the mail specifications * (Internet Message Format and MIME):

  • The * content-transfer-encoding "quoted-printable" is treated as 7bit * instead if it occurs in a message or body part with content type * "multipart/*" or "message/*" (other than "message/global", * "message/global-headers", "message/global-disposition-notification", * or "message/global-delivery-status").
  • If a message has two * or more Content-Type header fields, it is treated as having a content * type of "application/octet-stream", unless one or more of the header * fields is syntactically invalid.
  • Non-UTF-8 bytes appearing * in header field values are replaced with replacement characters. * Moreover, UTF-8 is parsed everywhere in header field values, even in * those parts of some structured header fields where this appears not * to be allowed.
  • The To and Cc header fields are allowed to * contain only comments and whitespace, but these "empty" header fields * will be omitted when generating.
  • There is no line length * limit imposed when parsing quoted-printable or base64 encoded * bodies.
  • If the transfer encoding is absent and the content * type is "message/rfc822", 8-bit bytes are still allowed, despite the * default value of "7bit" for "Content-Transfer-Encoding".
  • In * the following cases, if the transfer encoding is absent or ((declared * instanceof 7bit) ? (7bit)declared : null), 8-bit bytes are still * allowed:
  • (a) The preamble and epilogue of multipart * messages, which will be ignored.
  • (b) If the charset is * declared to be utf-8.
  • (c) If the content type is * "text/html" and the charset is declared to be ascii, * us-ascii, "windows-1252", "windows-1251", or "iso-8859-*" (all * single byte encodings).
  • (d) In non-MIME message bodies and * in text/plain message bodies. Any 8-bit bytes are replaced with the * ASCII substitute character (0x1a).
  • If the first line of the * message starts with the word "From" followed by a space, it is * skipped.
  • The name ascii is treated as a synonym for * us-ascii, despite being a reserved name under RFC 2046. The * name cp1252 is treated as a synonym for windows-1252 , * even though it's not an IANA registered alias.
  • The following * deviations involve encoded words under RFC 2047:
  • (a) If a * sequence of encoded words decodes to a string with a CTL character (U * + 007F, or a character less than U + 0020 and not TAB) after being * converted to Unicode, the encoded words are left un-decoded.
  • *
  • (b) This implementation can decode an encoded word that uses * ISO-2022-JP (the only supported encoding that uses code switching) * even if the encoded word's payload ends in a different mode from * ASCII mode. (Each encoded word still starts in ASCII mode, * though.)
*/ public final class Message { private static final int EncodingSevenBit = 0; private static final int EncodingUnknown = -1; private static final int EncodingEightBit = 3; private static final int EncodingBinary = 4; private static final int EncodingQuotedPrintable = 1; private static final int EncodingBase64 = 2; private static final boolean UseLenientLineBreaks = true; private List headers; private List parts; /** * Gets a list of all the parts of this message. This list is editable. This * will only be used if the message is a multipart message. * @return A list of all the parts of this message. This list is editable. This * will only be used if the message is a multipart message. */ public final List getParts() { return this.parts; } /** * Gets a snapshot of the header fields of this message, in the order they were * added. For each item in the list, the key is the header field's name * and the value is its value. * @return A snapshot of the header fields of this message. */ public final List> getHeaderFields() { ArrayList> list = new ArrayList>(); for (int i = 0; i < this.headers.size(); i += 2) { list.add( new AbstractMap.SimpleImmutableEntry( this.headers.get(i), this.headers.get(i + 1))); } return list; } /** * Gets the name and value of a header field by index. * @param index Zero-based index of the header field to get. * @return A Map.Entry object. The key is the name of the header field, such * as "From" or "Content-ID". The value is the header field's value. * @throws IllegalArgumentException The parameter {@code index} is 0 or at least as * high as the number of header fields. */ public Map.Entry GetHeader(int index) { if (index < 0) { throw new IllegalArgumentException("index (" + index + ") is less than " + "0"); } if (index >= (this.headers.size() / 2)) { throw new IllegalArgumentException("index (" + index + ") is not less than " + (this.headers.size() / 2)); } return new AbstractMap.SimpleImmutableEntry( this.headers.get(index), this.headers.get(index + 1)); } /** * Removes a header field by index. * @param index Zero-based index of the header field to set. * @return This instance. * @throws IllegalArgumentException The parameter {@code index} is 0 or at least as * high as the number of header fields. */ public Message RemoveHeader(int index) { if (index < 0) { throw new IllegalArgumentException("index (" + index + ") is less than " + "0"); } if (index >= (this.headers.size() / 2)) { throw new IllegalArgumentException("index (" + index + ") is not less than " + (this.headers.size() / 2)); } this.headers.remove(index * 2); this.headers.remove(index * 2); return this; } /** * Adds a header field to the end of the message's header. * @param header A Map.Entry object. The key is the name of the header * field, such as "From" or "Content-ID". The value is the header * field's value. * @return This instance. * @throws NullPointerException The key or value of {@code header} is null. */ public Message AddHeader(Map.Entry header) { return this.AddHeader(header.getKey(), header.getValue()); } /** * Adds a header field to the end of the message's header. * @param name Name of a header field, such as "From" or "Content-ID". * @param value Value of the header field. * @return This instance. * @throws NullPointerException The parameter {@code name} or {@code value} is * null. */ public Message AddHeader(String name, String value) { name = ValidateHeaderField(name, value); this.headers.add(name); this.headers.add(value); return this; } /** * Sets the name and value of a header field by index. * @param index Zero-based index of the header field to set. * @param header A Map.Entry object. The key is the name of the header * field, such as "From" or "Content-ID". The value is the header * field's value. * @return A Message object. * @throws IllegalArgumentException The parameter {@code index} is 0 or at least as * high as the number of header fields. * @throws IllegalArgumentException The parameter {@code index} is 0 or at least as * high as the number of header fields. * @throws NullPointerException The key or value of {@code header} is null. */ public Message SetHeader(int index, Map.Entry header) { return this.SetHeader(index, header.getKey(), header.getValue()); } /** * Sets the name and value of a header field by index. * @param index Zero-based index of the header field to set. * @param name Name of a header field, such as "From" or "Content-ID". * @param value Value of the header field. * @return This instance. * @throws IllegalArgumentException The parameter {@code index} is 0 or at least as * high as the number of header fields. * @throws NullPointerException The parameter {@code name} or {@code value} is * null. */ public Message SetHeader(int index, String name, String value) { if (index < 0) { throw new IllegalArgumentException("index (" + index + ") is less than " + "0"); } if (index >= (this.headers.size() / 2)) { throw new IllegalArgumentException("index (" + index + ") is not less than " + (this.headers.size() / 2)); } name = ValidateHeaderField(name, value); this.headers.set(index * 2, name); this.headers.set((index * 2) + 1, value); return this; } /** * Sets the value of a header field by index without changing its name. * @param index Zero-based index of the header field to set. * @param value Value of the header field. * @return This instance. * @throws IllegalArgumentException The parameter {@code index} is 0 or at least as * high as the number of header fields. * @throws NullPointerException The parameter {@code value} is null. */ public Message SetHeader(int index, String value) { if (index < 0) { throw new IllegalArgumentException("index (" + index + ") is less than " + "0"); } if (index >= (this.headers.size() / 2)) { throw new IllegalArgumentException("index (" + index + ") is not less than " + (this.headers.size() / 2)); } String name = ValidateHeaderField(this.headers.get(index * 2), value); this.headers.set(index * 2, name); this.headers.set((index * 2) + 1, value); return this; } private byte[] body; /** * Gets the byte array for this message's body. * @return A byte array. */ public byte[] GetBody() { return this.body; } /** * Sets the body of this message to the given byte array. * @param bytes A byte array. * @throws NullPointerException The parameter {@code bytes} is null. */ public void SetBody(byte[] bytes) { if (bytes == null) { throw new NullPointerException("bytes"); } this.body = bytes; } private static boolean IsShortAndAllAscii(String str) { if (str.length() > 0x10000) { return false; } for (int i = 0; i < str.length(); ++i) { if ((((int)str.charAt(i)) >> 7) != 0) { return false; } } return true; } /** * Sets the body of this message to the specified plain text string. The * character sequences CR, LF, and CR/LF will be converted to CR/LF line * breaks. Unpaired surrogate code points will be replaced with * replacement characters. * @param str A string consisting of the message in plain text format. * @return This instance. * @throws NullPointerException The parameter {@code str} is null. */ public Message SetTextBody(String str) { if (str == null) { throw new NullPointerException("str"); } this.body = DataUtilities.GetUtf8Bytes(str, true, true); this.contentType = IsShortAndAllAscii(str) ? MediaType.TextPlainAscii : MediaType.TextPlainUtf8; return this; } /** * Sets the body of this message to the specified string in HTML format. The * character sequences CR, LF, and CR/LF will be converted to CR/LF line * breaks. Unpaired surrogate code points will be replaced with * replacement characters. * @param str A string consisting of the message in HTML format. * @return This instance. * @throws NullPointerException The parameter {@code str} is null. */ public Message SetHtmlBody(String str) { if (str == null) { throw new NullPointerException("str"); } this.body = DataUtilities.GetUtf8Bytes(str, true, true); this.contentType = IsShortAndAllAscii(str) ? MediaType.TextPlainAscii : MediaType.TextPlainUtf8; return this; } /** * Sets the body of this message to a multipart body with plain text and HTML * versions of the same message. The character sequences CR, LF, and * CR/LF will be converted to CR/LF line breaks. Unpaired surrogate code * points will be replaced with replacement characters. * @param text A string consisting of the plain text version of the message. * @param html A string consisting of the HTML version of the message. * @return This instance. * @throws NullPointerException The parameter {@code text} or {@code html} is * null. */ public Message SetTextAndHtml(String text, String html) { if (text == null) { throw new NullPointerException("text"); } if (html == null) { throw new NullPointerException("html"); } // The spec for multipart/alternative (RFC 2046) says that // the fanciest version of the message should go last (in // this case, the HTML version) Message textMessage = new Message().SetTextBody(text); Message htmlMessage = new Message().SetHtmlBody(html); this.contentType = MediaType.Parse("multipart/alternative; boundary=\"=_boundary\""); List messageParts = this.getParts(); messageParts.clear(); messageParts.add(textMessage); messageParts.add(htmlMessage); return this; } /** * Gets a list of addresses found in the From header field or fields. * @return A list of addresses found in the From header field or fields. */ public final List getFromAddresses() { return ParseAddresses(this.GetMultipleHeaders("from")); } private boolean IsValidAddressingField(String name) { name = DataUtilities.ToLowerCaseAscii(name); boolean have = false; for (int i = 0; i < this.headers.size(); i += 2) { if (this.headers.get(i).equals(name)) { if (have) { return false; } String headerValue = this.headers.get(i + 1); if ( HeaderFieldParsers.GetParser( name).Parse( headerValue, 0, headerValue.length(), null) != headerValue.length()) { return false; } have = true; } } return true; } static List ParseAddresses(String value) { Tokener tokener = new Tokener(); if (value == null) { return new ArrayList(); } // Check for valid syntax return (HeaderParser.ParseHeaderTo(value, 0, value.length(), tokener) != value.length()) ? (new ArrayList()) : HeaderParserUtility.ParseAddressList( value, 0, value.length(), tokener.GetTokens()); } static List ParseAddresses(String[] values) { Tokener tokener = new Tokener(); ArrayList list = new ArrayList(); for (String addressValue : values) { if (addressValue == null) { continue; } if ( HeaderParser.ParseHeaderTo( addressValue, 0, addressValue.length(), tokener) != addressValue.length()) { // Invalid syntax continue; } list.addAll( HeaderParserUtility.ParseAddressList( addressValue, 0, addressValue.length(), tokener.GetTokens())); } return list; } /** * Gets a list of addresses found in the To header field or fields. * @return A list of addresses found in the To header field or fields. */ public final List getToAddresses() { return ParseAddresses(this.GetMultipleHeaders("to")); } /** * Gets a list of addresses found in the CC header field or fields. * @return A list of addresses found in the CC header field or fields. */ public final List getCCAddresses() { return ParseAddresses(this.GetMultipleHeaders("cc")); } /** * Gets a list of addresses found in the BCC header field or fields. * @return A list of addresses found in the BCC header field or fields. */ public final List getBccAddresses() { return ParseAddresses(this.GetMultipleHeaders("bcc")); } /** * Gets this message's subject. * @return This message's subject. */ public final String getSubject() { return this.GetHeader("subject"); } public final void setSubject(String value) { this.SetHeader("subject", value); } /** * Gets the body of this message as a Unicode string. * @return The body of this message as a Unicode string. * @throws UnsupportedOperationException This message has no character encoding * declared on it, or the character encoding is not supported. */ public final String getBodyString() { ICharacterEncoding charset = Encodings.GetEncoding( this.getContentType().GetCharset(), true); if (charset == null) { throw new UnsupportedOperationException("Not in a supported character set."); } return Encodings.DecodeToString( charset, DataIO.ToTransform(this.body)); } /** * Initializes a new instance of the Message class. Reads from the given InputStream * object to initialize the message. * @param stream A readable data stream. * @throws NullPointerException The parameter {@code stream} is null. */ public Message (InputStream stream) { if (stream == null) { throw new NullPointerException("stream"); } this.headers = new ArrayList(); this.parts = new ArrayList(); this.body = new byte[0]; IByteReader transform = DataIO.ToTransform(stream); // if (useLenientLineBreaks) { // TODO: Might not be correct if the transfer // encoding turns out to be binary // transform = new LineBreakNormalizeTransform(stream); // } this.ReadMessage(transform); } /** * Initializes a new instance of the Message class. Reads from the given byte * array to initialize the message. * @param bytes A readable data stream. * @throws NullPointerException The parameter {@code bytes} is null. */ public Message (byte[] bytes) { if (bytes == null) { throw new NullPointerException("bytes"); } this.headers = new ArrayList(); this.parts = new ArrayList(); this.body = new byte[0]; IByteReader transform = DataIO.ToTransform(bytes); this.ReadMessage(transform); } /** * Initializes a new instance of the Message class. The message will be plain * text and have an artificial From address. */ public Message () { this.headers = new ArrayList(); this.parts = new ArrayList(); this.body = new byte[0]; this.contentType = MediaType.TextPlainUtf8; this.headers.add("message-id"); this.headers.add(this.GenerateMessageID()); this.headers.add("from"); this.headers.add("[email protected]"); this.headers.add("mime-version"); this.headers.add("1.0"); } private static java.util.Random msgidRandom = new java.util.Random(); private static boolean seqFirstTime = true; private static int msgidSequence; private static Object sequenceSync = new Object(); private String GenerateMessageID() { long ticks = new java.util.Date().getTime(); StringBuilder builder = new StringBuilder(); int seq = 0; builder.append("<"); synchronized (sequenceSync) { if (seqFirstTime) { msgidSequence = msgidRandom.nextInt(65536); msgidSequence <<= 16; msgidSequence |= msgidRandom.nextInt(65536); seqFirstTime = false; } seq = (msgidSequence++); } String guid = java.util.UUID.randomUUID().toString(); String hex = "0123456789abcdef"; for (int i = 0; i < 16; ++i) { builder.append(hex.charAt((int)(ticks & 15))); ticks >>= 4; } for (int i = 0; i < guid.length(); ++i) { if (guid.charAt(i) != '-') { builder.append(guid.charAt(i)); } } for (int i = 0; i < 8; ++i) { builder.append(hex.charAt(seq & 15)); seq >>= 4; } List addresses = this.getFromAddresses(); if (addresses == null || addresses.size() == 0) { builder.append("@local.invalid"); } else { builder.append("@"); seq = addresses.get(0).isGroup() ? addresses.get(0).getName().hashCode() : addresses.get(0).getAddress().toString().hashCode(); for (int i = 0; i < 8; ++i) { builder.append(hex.charAt(seq & 15)); seq >>= 4; } builder.append(".local.invalid"); } builder.append(">"); return builder.toString(); } /** * Returns the mail message contained in this message's body. * @return A message object if this object's content type is "message/rfc822", * "message/news", or "message/global", or null otherwise. */ public Message GetBodyMessage() { if (this.getContentType().getTopLevelType().equals("message") && (this.getContentType().getSubType().equals("rfc822") || this.getContentType().getSubType().equals("news") || this.getContentType().getSubType().equals("global"))) { java.io.ByteArrayInputStream ms = null; try { ms = new java.io.ByteArrayInputStream(this.body); return new Message(ms); } finally { try { if (ms != null)ms.close(); } catch (java.io.IOException ex) {} } } return null; } private MediaType contentType; private ContentDisposition contentDisposition; private int transferEncoding; /** * Gets this message's media type. * @return This message's media type. * @throws NullPointerException This value is being set and "value" is null. */ public final MediaType getContentType() { return this.contentType; } public final void setContentType(MediaType value) { if (value == null) { throw new NullPointerException("value"); } if (!this.getContentType().equals(value)) { this.contentType = value; if (!value.isMultipart()) { List parts = this.getParts(); parts.clear(); } this.SetHeader("content-type", this.contentType.toString()); } } /** * Gets this message's content disposition. The content disposition specifies * how a user agent should handle or otherwise display this message. * @return This message's content disposition, or null if none is specified. */ public final ContentDisposition getContentDisposition() { return this.contentDisposition; } public final void setContentDisposition(ContentDisposition value) { if (value == null) { this.contentDisposition = null; this.RemoveHeader("content-disposition"); } else if (!value.equals(this.contentDisposition)) { this.contentDisposition = value; this.SetHeader("content-disposition", this.contentDisposition.toString()); } } /** * Gets a filename suggested by this message for saving the message's body to a * file. For more information on the algorithm, see * ContentDisposition.MakeFilename. * @return A suggested name for the file, or the empty string if there is no * filename suggested by the content type or content disposition. */ public final String getFileName() { ContentDisposition disp = this.contentDisposition; return (disp != null) ? ContentDisposition.MakeFilename(disp.GetParameter("filename")) : ContentDisposition.MakeFilename(this.contentType.GetParameter("name" )); } private void ProcessHeaders(boolean assumeMime, boolean digest) { boolean haveContentType = false; boolean mime = assumeMime; boolean haveContentDisp = false; String transferEncodingValue = ""; for (int i = 0; i < this.headers.size(); i += 2) { String name = this.headers.get(i); String value = this.headers.get(i + 1); if (name.equals("content-transfer-encoding")) { int startIndex = HeaderParser.ParseCFWS(value, 0, value.length(), null); // NOTE: Actually "token", but all known transfer encoding values // fit the same syntax as the stricter one for top-level types and // subtypes int endIndex = MediaType.skipMimeTypeSubtype( value, startIndex, value.length(), null); transferEncodingValue = ( HeaderParser.ParseCFWS( value, endIndex, value.length(), null) == value.length()) ? value.substring(startIndex, (startIndex)+(endIndex - startIndex)) : ""; } mime |= name.equals("mime-version"); if (value.indexOf("=?") >= 0) { IHeaderFieldParser parser = HeaderFieldParsers.GetParser(name); // Decode encoded words in the header field where possible value = parser.DecodeEncodedWords(value); this.headers.set(i + 1, value); } } this.contentType = digest ? MediaType.MessageRfc822 : MediaType.TextPlainAscii; boolean haveInvalid = false; boolean haveContentEncoding = false; for (int i = 0; i < this.headers.size(); i += 2) { String name = this.headers.get(i); String value = this.headers.get(i + 1); if (mime && name.equals("content-transfer-encoding")) { value = DataUtilities.ToLowerCaseAscii(transferEncodingValue); this.headers.set(i + 1, value); if (value.equals("7bit")) { this.transferEncoding = EncodingSevenBit; } else if (value.equals("8bit")) { this.transferEncoding = EncodingEightBit; } else if (value.equals("binary")) { this.transferEncoding = EncodingBinary; } else if (value.equals("quoted-printable")) { this.transferEncoding = EncodingQuotedPrintable; } else if (value.equals("base64")) { this.transferEncoding = EncodingBase64; } else { // Unrecognized transfer encoding this.transferEncoding = EncodingUnknown; } haveContentEncoding = true; } else if (mime && name.equals("content-type")) { if (haveContentType) { // DEVIATION: If there is already a content type, // treat content type as application/octet-stream if (haveInvalid || MediaType.Parse(value, null) == null) { this.contentType = MediaType.TextPlainAscii; haveInvalid = true; } else { this.contentType = MediaType.ApplicationOctetStream; } } else { this.contentType = MediaType.Parse( value, null); if (this.contentType == null) { this.contentType = digest ? MediaType.MessageRfc822 : MediaType.TextPlainAscii; haveInvalid = true; } haveContentType = true; } } else if (mime && name.equals("content-disposition")) { if (haveContentDisp) { String valueExMessage = "Already have this header: " + name; throw new MessageDataException(valueExMessage); } this.contentDisposition = ContentDisposition.Parse(value); haveContentDisp = true; } } if (this.transferEncoding == EncodingUnknown) { this.contentType = MediaType.Parse("application/octet-stream"); } if (!haveContentEncoding && this.contentType.getTypeAndSubType().equals( "message/rfc822")) { // DEVIATION: Be a little more liberal with rfc822 // messages with 8-bit bytes this.transferEncoding = EncodingEightBit; } if (this.transferEncoding == EncodingSevenBit) { String charset = this.contentType.GetCharset(); if (charset.equals("utf-8")) { // DEVIATION: Be a little more liberal with UTF-8 this.transferEncoding = EncodingEightBit; } else if (this.contentType.getTypeAndSubType().equals("text/html")) { if (charset.equals("us-ascii") || charset.equals("ascii") || charset.equals("windows-1252") || charset.equals( "windows-1251") || (charset.length() > 9 && charset.substring(0, 9).equals( "iso-8859-"))) { // DEVIATION: Be a little more liberal with text/html and // single-byte charsets or UTF-8 this.transferEncoding = EncodingEightBit; } } } if (this.transferEncoding == EncodingQuotedPrintable || this.transferEncoding == EncodingBase64 || this.transferEncoding == EncodingUnknown) { if (this.contentType.isMultipart() || (this.contentType.getTopLevelType().equals("message") && !this.contentType.getSubType().equals("global") && !this.contentType.getSubType().equals("global-headers") && !this.contentType.getSubType().equals( "global-disposition-notification") && !this.contentType.getSubType().equals("global-delivery-status"))) { if (this.transferEncoding == EncodingQuotedPrintable) { // DEVIATION: Treat quoted-printable for multipart and message // as 7bit instead this.transferEncoding = EncodingSevenBit; } else { String exceptText = "Invalid content encoding for multipart or message"; throw new MessageDataException(exceptText); } } } } private static boolean IsWellFormedBoundary(String str) { if (str == null || str.length() < 1 || str.length() > 70) { return false; } for (int i = 0; i < str.length(); ++i) { char c = str.charAt(i); if (i == str.length() - 1 && c == 0x20) { // Space not allowed at the end of a boundary return false; } if (!( (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == 0x20 || c == 0x2c || "'()-./+_:=?" .indexOf(c) >= 0)) { return false; } } return true; } /** * Gets the first instance of the header field with the specified name, * comparing the field name in an ASCII case-insensitive manner. * @param name The name of a header field. * @return The value of the first header field with that name, or null if there * is none. * @throws NullPointerException Name is null. */ public String GetHeader(String name) { if (name == null) { throw new NullPointerException("name"); } name = DataUtilities.ToLowerCaseAscii(name); for (int i = 0; i < this.headers.size(); i += 2) { if (this.headers.get(i).equals(name)) { // Get the first instance of the header field return this.headers.get(i + 1); } } return null; } private static String Implode(String[] strings, String delim) { if (strings.length == 0) { return ""; } if (strings.length == 1) { return strings[0]; } StringBuilder sb = new StringBuilder(); boolean first = true; for (String s : strings) { if (!first) { sb.append(delim); } sb.append(s); first = false; } return sb.toString(); } private String[] GetMultipleHeaders(String name) { ArrayList headerList = new ArrayList(); name = DataUtilities.ToLowerCaseAscii(name); for (int i = 0; i < this.headers.size(); i += 2) { if (this.headers.get(i).equals(name)) { headerList.add(this.headers.get(i + 1)); } } return headerList.toArray(new String[] { }); } // Returns true only if: // * Text matches the production "unstructured" // in RFC 5322 without any obsolete syntax // * Each line is no more than 75 characters in length // * Text has only printable ASCII characters, CR, // LF, and/or TAB static boolean CanOutputRaw(String s) { int len = s.length(); int chunkLength = 0; boolean maybe = false; boolean firstColon = true; for (int i = 0; i < len; ++i) { char c = s.charAt(i); if (c == ':' && firstColon) { if (i + 1 >= len || s.charAt(i + 1) != 0x20) { // colon not followed by SPACE (0x20) return false; } firstColon = false; } if (c == 0x0d) { if (i + 1 >= len || s.charAt(i + 1) != 0x0a) { // bare CR return false; } if (i + 2 >= len || (s.charAt(i + 2) != 0x09 && s.charAt(i + 2) != 0x20)) { // CRLF not followed by whitespace return false; } chunkLength = 0; maybe = true; i += 2; continue; } if (c >= 0x7f || (c < 0x20 && c != 0x09 && c != 0x0d)) { // CTLs (except TAB, SPACE, and CR) and non-ASCII // characters return false; } ++chunkLength; if (chunkLength > 75) { return false; } } return (!maybe) || (ParseUnstructuredText(s, 0, s.length()) == s.length()); } private static String Capitalize(String s) { StringBuilder builder = new StringBuilder(); boolean afterHyphen = true; for (int i = 0; i < s.length(); ++i) { if (afterHyphen && s.charAt(i) >= 'a' && s.charAt(i) <= 'z') { builder.append((char)(s.charAt(i) - 0x20)); } else { builder.append(s.charAt(i)); } afterHyphen = s.charAt(i) == '-'; } String ret = builder.toString(); return ret.equals("Mime-Version") ? "MIME-Version" : (ret.equals("Message-Id") ? "Message-ID" : ret); } static boolean HasTextToEscape(String s) { // Returns true if the String has: * non-ASCII characters * // "=?" * CTLs other than tab, or * a word longer than 75 characters. // Can return false even if the String has: * CRLF followed by a line // with just whitespace. return HasTextToEscape(s, 0, s.length()); } static boolean HasTextToEscape(String s, int index, int endIndex) { int len = endIndex; int chunkLength = 0; for (int i = index; i < endIndex; ++i) { char c = s.charAt(i); if (c == '=' && i + 1 < len && c == '?') { // "=?" (start of an encoded word) return true; } if (c == 0x0d) { if (i + 1 >= len || s.charAt(i + 1) != 0x0a) { // bare CR // System.out.println("bare CR"); return true; } if (i + 2 >= len || (s.charAt(i + 2) != 0x09 && s.charAt(i + 2) != 0x20)) { // CRLF not followed by whitespace return true; } chunkLength = 0; ++i; continue; } if (c == 0x0a) { // bare LF return true; } if (c >= 0x7f || (c < 0x20 && c != 0x09 && c != 0x0d)) { // CTLs (except TAB, SPACE, and CR) and non-ASCII // characters return true; } if (c == 0x20 || c == 0x09) { chunkLength = 0; } else { ++chunkLength; if (chunkLength > 75) { return true; } } } return false; } static boolean HasTextToEscapeIgnoreEncodedWords( String s, int index, int endIndex) { int len = endIndex; int chunkLength = 0; for (int i = index; i < endIndex; ++i) { char c = s.charAt(i); if (c == 0x0d) { if (i + 1 >= len || s.charAt(i + 1) != 0x0a) { // bare CR // System.out.println("bare CR"); return true; } if (i + 2 >= len || (s.charAt(i + 2) != 0x09 && s.charAt(i + 2) != 0x20)) { // CRLF not followed by whitespace return true; } chunkLength = 0; ++i; continue; } if (c == 0x0a) { // bare LF return true; } if (c >= 0x7f || (c < 0x20 && c != 0x09 && c != 0x0d)) { // CTLs (except TAB, SPACE, and CR) and non-ASCII // characters return true; } if (c == 0x20 || c == 0x09) { chunkLength = 0; } else { ++chunkLength; if (chunkLength > 997) { return true; } } } return false; } static int ParseUnstructuredText( String str, int index, int endIndex) { int indexTemp = index; do { while (true) { int indexTemp2 = index; do { int indexStart2 = index; do { int indexTemp3 = index; do { int indexStart3 = index; do { int indexTemp4; indexTemp4 = index; do { int indexStart4 = index; while (index < endIndex && ((str.charAt(index) == 32) || (str.charAt(index) == 9))) { ++index; } if (index + 1 < endIndex && str.charAt(index) == 13 && str.charAt(index + 1) == 10) { index += 2; } else { index = indexStart4; break; } indexTemp4 = index; index = indexStart4; } while (false); if (indexTemp4 != index) { index = indexTemp4; } else { break; } } while (false); if (index < endIndex && ((str.charAt(index) == 32) || (str.charAt(index) == 9))) { ++index; while (index < endIndex && ((str.charAt(index) == 32) || (str.charAt(index) == 9))) { ++index; } } else { index = indexStart3; break; } indexTemp3 = index; index = indexStart3; } while (false); if (indexTemp3 != index) { index = indexTemp3; } else { break; } } while (false); do { int indexTemp3 = index; do { if (index < endIndex && ((str.charAt(index) >= 128 && str.charAt(index) <= 55295) || (str.charAt(index) >= 57344 && str.charAt(index) <= 65535))) { ++indexTemp3; break; } if (index + 1 < endIndex && ((str.charAt(index) >= 55296 && str.charAt(index) <= 56319) && (str.charAt(index + 1) >= 56320 && str.charAt(index + 1) <= 57343))) { indexTemp3 += 2; break; } if (index < endIndex && (str.charAt(index) >= 33 && str.charAt(index) <= 126)) { ++indexTemp3; break; } } while (false); if (indexTemp3 != index) { index = indexTemp3; } else { index = indexStart2; break; } } while (false); if (index == indexStart2) { break; } indexTemp2 = index; index = indexStart2; } while (false); if (indexTemp2 != index) { index = indexTemp2; } else { break; } } while (index < endIndex && ((str.charAt(index) == 32) || (str.charAt(index) == 9))) { ++index; } indexTemp = index; } while (false); return indexTemp; } private static String ValidateHeaderField(String name, String value) { if (name == null) { throw new NullPointerException("name"); } if (value == null) { throw new NullPointerException("value"); } if (name.length() > 997) { throw new IllegalArgumentException("Header field name too long"); } name = DataUtilities.ToLowerCaseAscii(name); for (int i = 0; i < name.length(); ++i) { if (name.charAt(i) <= 0x20 || name.charAt(i) == ':' || name.charAt(i) >= 0x7f) { throw new IllegalArgumentException("Header field name contains an invalid character"); } } // Check characters in structured header fields IHeaderFieldParser parser = HeaderFieldParsers.GetParser(name); if (parser.IsStructured()) { if (ParseUnstructuredText(value, 0, value.length()) != value.length()) { throw new IllegalArgumentException("Header field value contains invalid text"); } if (parser.Parse(value, 0, value.length(), null) != value.length()) { throw new IllegalArgumentException("Header field value is not in the correct format"); } } return name; } /** * Sets the value of this message's header field. If a header field with the * same name exists, its value is replaced. * @param name The name of a header field, such as "from" or "subject". * @param value The header field's value. * @return This instance. * @throws IllegalArgumentException The header field name is too long or contains an * invalid character, or the header field's value is syntactically * invalid. * @throws NullPointerException The parameter {@code name} or {@code value} is * null. */ public Message SetHeader(String name, String value) { name = ValidateHeaderField(name, value); // Add the header field for (int i = 0; i < this.headers.size(); i += 2) { if (this.headers.get(i).equals(name)) { this.headers.set(i + 1, value); return this; } } this.headers.add(name); this.headers.add(value); return this; } /** * Removes all instances of the given header field from this message. If this * is a multipart message, the header field is not removed from its body * part headers. * @param name The name of the header field to remove. * @return This instance. * @throws NullPointerException The parameter {@code name} is null. */ public Message RemoveHeader(String name) { if (name == null) { throw new NullPointerException("name"); } name = DataUtilities.ToLowerCaseAscii(name); // Remove the header field for (int i = 0; i < this.headers.size(); i += 2) { if (this.headers.get(i).equals(name)) { this.headers.remove(i); this.headers.remove(i); i -= 2; } } return this; } private static boolean StartsWithWhitespace(String str) { return str.length() > 0 && (str.charAt(0) == ' ' || str.charAt(0) == 0x09 || str.charAt(0) == '\r'); } private static int TransferEncodingToUse(byte[] body, boolean isBodyPart) { if (body == null || body.length == 0) { return EncodingSevenBit; } int lengthCheck = Math.min(body.length, 4096); int highBytes = 0; int ctlBytes = 0; int lineLength = 0; boolean allTextBytes = !isBodyPart; for (int i = 0; i < lengthCheck; ++i) { if ((body[i] & 0x80) != 0) { ++highBytes; allTextBytes = false; } else if (body[i] == 0x00) { allTextBytes = false; ++ctlBytes; } else if (body[i] == 0x7f || (body[i] < 0x20 && body[i] != 0x0d && body[i] != 0x0a && body[i] != 0x09)) { allTextBytes = false; ++ctlBytes; } else if (body[i] == (byte)'\r') { if (i + 1 >= body.length || body[i + 1] != (byte)'\n') { // bare CR allTextBytes = false; } else if (i > 0 && (body[i - 1] == (byte)' ' || body[i - 1] == (byte)'\t' )) { // Space followed immediately by CRLF allTextBytes = false; } else { ++i; lineLength = 0; continue; } } else { allTextBytes &= body[i] != (byte)'\n'; } allTextBytes &= lineLength != 0 || i + 2 >= body.length || body[i] != '.' || body[i + 1] != '\r' || body[i + 2] != '\n'; allTextBytes &= lineLength != 0 || i + 4 >= body.length || body[i] != 'F' || body[i + 1] != 'r' || body[i + 2] != 'o' || body[i + 3] != 'm' || body[i + 4] != ' '; ++lineLength; allTextBytes &= lineLength <= 78; } return (lengthCheck == body.length && allTextBytes) ? EncodingSevenBit : ((highBytes > (lengthCheck / 3)) ? EncodingBase64 : ((ctlBytes > 10) ? EncodingBase64 : EncodingQuotedPrintable)); } static String GenerateAddressList(List list) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < list.size(); ++i) { if (i > 0) { sb.append(", "); } sb.append(list.get(i).toString()); } return sb.toString(); } static boolean CanBeUnencoded( byte[] bytes, boolean checkBoundaryDelimiter) { if (bytes == null || bytes.length == 0) { return true; } int lineLength = 0; int index = 0; int endIndex = bytes.length; boolean headers = true; while (index < endIndex) { int c = ((int)bytes[index]) & 0xff; if (c >= 0x80) { // System.out.println("Non-ASCII character (0x {0:X2})",(int)c); return false; } if (lineLength == 0 && checkBoundaryDelimiter && index + 4 < endIndex && bytes[index] == '-' && bytes[index + 1] == '-' && bytes[index + 2] == '=' && bytes[index + 3] == '_' && bytes[index + 4] == 'B') { // Start of a reserved boundary delimiter return false; } if (c == '\r' && index + 1 < endIndex && bytes[index + 1] == '\n') { index += 2; if (headers && lineLength == 0) { // Start of the body headers = false; } lineLength = 0; continue; } if (c == '\r' || c == '\n') { // System.out.println("Bare CR or bare LF"); return false; } ++lineLength; if (lineLength > 78) { // System.out.println("Line length exceeded (" + maxLineLength + // " " + (str.substring(index-78,(index-78)+(78))) + ")"); return false; } ++index; } return true; } /** * Generates this message's data in text form.

The generated message will * always be 7-bit ASCII, and the transfer encoding will always be 7bit, * quoted-printable, or base64 (the declared transfer encoding for this * message will be ignored).

The following applies to the From, * To, Cc, and Bcc header fields. If the header field exists, but has an * invalid syntax or has no addresses, this method will generate a * synthetic header field with the display-name set to the contents of * all of the header fields with the same name, and the address set to * me@[header-name]-address.invalid as the address (a * .invalid address is a reserved address that can never belong * to anyone). The generated message should always have a From header * field.

* @return The generated message. * @throws MessageDataException The message can't be generated. */ public String Generate() { ArrayWriter aw = new ArrayWriter(); this.Generate(aw, 0); return DataUtilities.GetUtf8String(aw.ToArray(), false); } private static String GenerateBoundary(int num) { StringBuilder sb = new StringBuilder(); String hex = "0123456789ABCDEF"; sb.append("=_Boundary"); for (int i = 0; i < 4; ++i) { int b = (num >> 56) & 255; sb.append(hex.charAt((b >> 4) & 15)); sb.append(hex.charAt(b & 15)); num <<= 8; } return sb.toString(); } private String SynthesizeField(String name) { String fullField = Implode(this.GetMultipleHeaders(name), ", "); String value = new EncodedWordEncoder().AddString(fullField) .FinalizeEncoding().toString(); if (value.length() > 0) { value += " "; } else { value = "me@" + name + "-address.invalid"; } return value; } private static Map MakeHeaderIndices() { HashMap dict = new HashMap(); dict.put("to",0); dict.put("cc",1); dict.put("bcc",2); dict.put("from",3); dict.put("reply-to",4); dict.put("resent-to",5); dict.put("resent-cc",6); dict.put("resent-bcc",7); dict.put("from",8); dict.put("sender",9); dict.put("resent-sender",10); return dict; } private static Map valueHeaderIndices = MakeHeaderIndices(); private static void AppendAscii(IWriter output, String str) { for (int i = 0; i < str.length(); ++i) { char c = str.charAt(i); if (c >= 0x80) { throw new MessageDataException("ascii expected"); } output.write((byte)c); } } private void Generate(IWriter output, int depth) { StringBuilder sb = new StringBuilder(); boolean haveMimeVersion = false; boolean haveContentEncoding = false; boolean haveContentType = false; boolean haveContentDisp = false; boolean haveMsgId = false; boolean[] haveHeaders = new boolean[11]; byte[] bodyToWrite = this.body; MediaTypeBuilder builder = new MediaTypeBuilder(this.getContentType()); String contentDisp = (this.getContentDisposition() == null) ? null : this.getContentDisposition().toString(); int transferEnc = 0; boolean isMultipart = false; String boundary = ""; if (builder.isMultipart()) { boundary = GenerateBoundary(depth); builder.SetParameter("boundary", boundary); isMultipart = true; } if (!isMultipart) { if (builder.getTopLevelType().equals("message")) { if (builder.getSubType().equals("delivery-status") || builder.getSubType().equals("global-delivery-status")) { bodyToWrite = DowngradeDeliveryStatus(bodyToWrite); } boolean msgCanBeUnencoded = CanBeUnencoded(bodyToWrite, depth > 0); if ((builder.getSubType().equals("rfc822") || builder.getSubType().equals( "news")) && !msgCanBeUnencoded) { builder.SetSubType("global"); } else if (builder.getSubType().equals("disposition-notification") && !msgCanBeUnencoded) { builder.SetSubType("global-disposition-notification"); } else if (builder.getSubType().equals("delivery-status") && !msgCanBeUnencoded) { builder.SetSubType("global-delivery-status"); } else if (!msgCanBeUnencoded) { throw new MessageDataException("Message body can't be encoded"); } } } String topLevel = builder.getTopLevelType(); if (topLevel.equals("message") || topLevel.equals("multipart")) { transferEnc = (topLevel.equals("multipart") || ( !builder.getSubType().equals("global") && !builder.getSubType().equals("global-headers") && !builder.getSubType().equals("global-disposition-notification") && !builder.getSubType().equals("global-delivery-status"))) ? EncodingSevenBit : TransferEncodingToUse( bodyToWrite, depth > 0); } else { transferEnc = TransferEncodingToUse(bodyToWrite, depth > 0); } String encodingString = "7bit"; if (transferEnc == EncodingBase64) { encodingString = "base64"; } else if (transferEnc == EncodingQuotedPrintable) { encodingString = "quoted-printable"; } // Write the header fields for (int i = 0; i < this.headers.size(); i += 2) { String name = this.headers.get(i); String value = this.headers.get(i + 1); if (name.equals("content-type")) { if (haveContentType) { // Already outputted, continue continue; } haveContentType = true; value = builder.toString(); } if (name.equals("content-disposition")) { if (haveContentDisp || contentDisp == null) { // Already outputted, continue continue; } haveContentDisp = true; value = contentDisp; } else if (name.equals("content-transfer-encoding")) { if (haveContentEncoding) { // Already outputted, continue continue; } haveContentEncoding = true; value = encodingString; } if ( depth > 0 && ( name.length() < 8 || !name.substring( 0, ( 0)+(8)).equals("content-"))) { // don't generate header fields not starting with "Content-" // in body parts continue; } if (name.equals("mime-version")) { haveMimeVersion = true; } else if (name.equals("message-id")) { if (haveMsgId) { // Already outputted, continue continue; } haveMsgId = true; } else { if (valueHeaderIndices.containsKey(name)) { int headerIndex = valueHeaderIndices.get(name); if (headerIndex < 8) { // TODO: Handle Sender, Resent-From, Resent-Sender if (haveHeaders[headerIndex]) { // Already outputted, continue continue; } haveHeaders[headerIndex] = true; if (!this.IsValidAddressingField(name)) { value = GenerateAddressList(ParseAddresses(this.GetMultipleHeaders(name))); if (value.length() == 0) { // No addresses, synthesize a field value = this.SynthesizeField(name); } } } } } String rawField = Capitalize(name) + ":" + (StartsWithWhitespace(value) ? "" : " ") + value; if (CanOutputRaw(rawField)) { AppendAscii(output, rawField); if (rawField.indexOf(": ") < 0) { throw new MessageDataException("No colon+space: " + rawField); } } else if (HasTextToEscape(value)) { String downgraded = HeaderFieldParsers.GetParser(name).DowngradeFieldValue(value); if ( HasTextToEscapeIgnoreEncodedWords( downgraded, 0, downgraded.length())) { if (name.equals("message-id") || name.equals("resent-message-id") || name.equals( "in-reply-to") || name.equals("references") || name.equals( "original-recipient") || name.equals("final-recipient")) { // Header field still contains invalid characters (such // as non-ASCII characters in 7-bit messages), convert // to a downgraded field name = "downgraded-" + name; downgraded = Rfc2047.EncodeString(ParserUtility.TrimSpaceAndTab(value)); } else { } } boolean haveDquote = downgraded.indexOf('"') >= 0; WordWrapEncoder encoder = new WordWrapEncoder( Capitalize(name) + ": ", !haveDquote); encoder.AddString(downgraded); String newValue = encoder.toString(); if (newValue.indexOf(": ") < 0) { throw new MessageDataException("No colon+space: " + newValue); } AppendAscii(output, newValue); } else { boolean haveDquote = value.indexOf('"') >= 0; WordWrapEncoder encoder = new WordWrapEncoder( Capitalize(name) + ": ", !haveDquote); encoder.AddString(value); String newValue = encoder.toString(); if (newValue.indexOf(": ") < 0) { throw new MessageDataException("No colon+space: " + newValue); } AppendAscii(output, newValue); } AppendAscii(output, "\r\n"); } if (true && depth == 0) { // Output a synthetic From field if it doesn't // exist and this isn't a body part AppendAscii(output, "From: [email protected]\r\n"); } if (!haveMsgId && depth == 0) { AppendAscii(output, "Message-ID: "); AppendAscii(output, this.GenerateMessageID()); AppendAscii(output, "\r\n"); } if (!haveMimeVersion && depth == 0) { AppendAscii(output, "MIME-Version: 1.0\r\n"); } if (!haveContentType) { AppendAscii(output, "Content-Type: " + builder + "\r\n"); } if (!haveContentEncoding) { AppendAscii(output, "Content-Transfer-Encoding: " + encodingString + "\r\n"); } ICharacterEncoder bodyEncoder = null; switch (transferEnc) { case EncodingBase64: bodyEncoder = new Base64Encoder(true, builder.isText(), false); break; case EncodingQuotedPrintable: bodyEncoder = new QuotedPrintableEncoder( builder.isText() ? 2 : 0, false); break; default: bodyEncoder = new IdentityEncoder(); break; } // Write the body AppendAscii(output, "\r\n"); if (!isMultipart) { int index = 0; while (true) { int c = (index >= bodyToWrite.length) ? -1 : ((int)bodyToWrite[index++]) & 0xff; int count = bodyEncoder.Encode(c, output); if (count == -2) { throw new MessageDataException("encoding error"); } if (count == -1) { break; } } } else { for (Message part : this.getParts()) { AppendAscii(output, "\r\n--" + boundary + "\r\n"); part.Generate(output, depth + 1); } AppendAscii(output, "\r\n--" + boundary + "--"); } } private static int ReadUtf8Char( TransformWithUnget stream, int[] bytesRead) { if (stream == null) { throw new NullPointerException("stream"); } int cp = 0; int bytesSeen = 0; int bytesNeeded = 0; int lower = 0x80; int upper = 0xbf; int read = 0; while (true) { int b = stream.read(); ++read; if (b < 0) { if (bytesNeeded != 0) { stream.Unget(); --read; bytesRead[0] = read; return 0xfffd; } return -1; } if (bytesNeeded == 0) { if ((b & 0x7f) == b) { bytesRead[0] = read; return b; } if (b >= 0xc2 && b <= 0xdf) { bytesNeeded = 1; cp = (b - 0xc0) << 6; } else if (b >= 0xe0 && b <= 0xef) { lower = (b == 0xe0) ? 0xa0 : 0x80; upper = (b == 0xed) ? 0x9f : 0xbf; bytesNeeded = 2; cp = (b - 0xe0) << 12; } else if (b >= 0xf0 && b <= 0xf4) { lower = (b == 0xf0) ? 0x90 : 0x80; upper = (b == 0xf4) ? 0x8f : 0xbf; bytesNeeded = 3; cp = (b - 0xf0) << 18; } else { bytesRead[0] = read; return 0xfffd; } continue; } if (b < lower || b > upper) { stream.Unget(); return 0xfffd; } lower = 0x80; upper = 0xbf; ++bytesSeen; cp += (b - 0x80) << (6 * (bytesNeeded - bytesSeen)); if (bytesSeen != bytesNeeded) { continue; } bytesRead[0] = read; return cp; } } static String DowngradeRecipientHeaderValue(String headerValue) { return DowngradeRecipientHeaderValue(headerValue, null); } static String DowngradeRecipientHeaderValue( String headerValue, int[] status) { int index; if ( HasTextToEscapeIgnoreEncodedWords( headerValue, 0, headerValue.length())) { index = HeaderParser.ParseCFWS( headerValue, 0, headerValue.length(), null); int atomText = HeaderParser.ParsePhraseAtom( headerValue, index, headerValue.length(), null); int typeEnd = atomText; String origValue = headerValue; boolean isUtf8 = typeEnd - index == 5 && (headerValue.charAt(index) & ~0x20) == 'U' && (headerValue.charAt(index + 1) & ~0x20) == 'T' && (headerValue.charAt(index + 2) & ~0x20) == 'F' && headerValue.charAt(index + 3) == '-' && headerValue.charAt(index + 4) == '8'; atomText = HeaderParser.ParseCFWS( headerValue, atomText, headerValue.length(), null); if (index < headerValue.length() && headerValue.charAt(atomText) == ';') { String typePart = headerValue.substring(0, atomText + 1); // Downgrade the comments in the type part // NOTE: Final-recipient has the same syntax as original-recipient, // except for the header field name typePart = HeaderFieldParsers.GetParser( "original-recipient").DowngradeFieldValue(typePart); if (isUtf8) { // Downgrade the non-ASCII characters in the address StringBuilder builder = new StringBuilder(); String hex = "0123456789ABCDEF"; for (int i = atomText + 1; i < headerValue.length(); ++i) { if (headerValue.charAt(i) < 0x80) { builder.append(headerValue.charAt(i)); } else { int cp = DataUtilities.CodePointAt(headerValue, i); if (cp >= 0x10000) { ++i; } builder.append("\\x"); builder.append('{'); for (int j = 20; j >= 0; j -= 4) { if ((cp >> j) != 0) { builder.append(hex.charAt((cp >> j) & 15)); } } builder.append('}'); } } headerValue = typePart + builder; } else { headerValue = typePart + headerValue.substring(atomText + 1); } } if ( HasTextToEscapeIgnoreEncodedWords( headerValue, 0, headerValue.length())) { // Encapsulate the header field in encoded words if (status != null) { // Encapsulated status[0] = 2; } return Rfc2047.EncodeString(ParserUtility.TrimSpaceAndTabLeft(origValue)); } if (status != null) { // Downgraded status[0] = 1; } return headerValue; } if (status != null) { status[0] = 0; // Unchanged } return headerValue; } // Parse the delivery status byte array to downgrade // the Original-Recipient and Final-Recipient header fields static byte[] DowngradeDeliveryStatus(byte[] bytes) { // int lineCount = 0; StringBuilder sb = new StringBuilder(); int index = 0; int endIndex = bytes.length; int lastIndex = -1; ArrayWriter writer = null; while (index < endIndex) { sb.delete(0, (0)+(sb.length())); boolean first = true; int headerNameStart = index; int headerNameEnd = index; // lineCount = 0; boolean endOfHeaders = false; while (true) { if (index >= endIndex) { // All headers read endOfHeaders = true; break; } int c = (index < endIndex) ? (((int)bytes[index]) & 0xff) : -1; // ++lineCount; ++index; if (c == '\r') { c = (index < endIndex) ? (((int)bytes[index]) & 0xff) : -1; ++index; if (c == '\n') { // lineCount = 0; headerNameStart = index; } else { --index; headerNameEnd = index; } continue; } if ((c >= 0x21 && c <= 57) || (c >= 59 && c <= 0x7e)) { first = false; if (c >= 'A' && c <= 'Z') { c += 0x20; } sb.append((char)c); } else if (!first && c == ':') { break; } else { first &= c != 0x20 && c != 0x09; } if (c != 0x20 && c != 0x09) { headerNameEnd = index; } } if (endOfHeaders) { break; } int headerValueStart = index; int headerValueEnd = index; String origFieldName = DataUtilities.GetUtf8String( bytes, headerNameStart, headerValueStart - headerNameStart, true); String fieldName = DataUtilities.ToLowerCaseAscii( DataUtilities.GetUtf8String( bytes, headerNameStart, headerNameEnd - headerNameStart, true)); boolean origRecipient = fieldName.equals("original-recipient"); boolean finalRecipient = fieldName.equals("final-recipient"); // Read the header field value using UTF-8 characters // rather than bytes while (true) { if (index >= endIndex) { // All headers read headerValueEnd = index; break; } int c = (index < endIndex) ? (((int)bytes[index]) & 0xff) : -1; ++index; if (c == '\r') { c = (index < endIndex) ? (((int)bytes[index]) & 0xff) : -1; ++index; if (c == '\n') { // lineCount = 0; // Parse obsolete folding whitespace (obs-fws) under RFC5322 // (parsed according to errata), same as LWSP in RFC5234 boolean fwsFirst = true; boolean haveFWS = false; boolean lineStart = true; while (true) { // Skip the CRLF pair, if any (except if iterating for // the first time, since CRLF was already parsed) if (!fwsFirst) { c = (index < endIndex) ? (((int)bytes[index]) & 0xff) : -1; ++index; if (c == '\r') { c = (index < endIndex) ? (((int)bytes[index]) & 0xff) : -1; ++index; if (c == '\n') { // CRLF was read lineStart = true; } else { // It's the first part of the line, where the header name // should be, so the CR here is illegal throw new MessageDataException("CR not followed by LF"); } } else { // anything else, unget --index; } } fwsFirst = false; // Use ReadByte here since we're just looking for the single // byte characters space and tab int c2 = (index < endIndex) ? (((int)bytes[index]) & 0xff) : -1; ++index; if (c2 == 0x20 || c2 == 0x09) { lineStart = false; haveFWS = true; } else { --index; // this isn't space or tab; if this is the staart // of the line, this is no longer FWS if (lineStart) { haveFWS = false; } break; } } if (haveFWS) { // We have folding whitespace, line // count found as above continue; } // This ends the header field // (the last two characters will be CRLF) headerValueEnd = index - 2; break; } --index; // ++lineCount; } // ++lineCount; } if (origRecipient || finalRecipient) { String headerValue = DataUtilities.GetUtf8String( bytes, headerValueStart, headerValueEnd - headerValueStart, true); int[] status = new int[1]; headerValue = DowngradeRecipientHeaderValue(headerValue, status); if (status[0] == 2 || status[0] == 1) { // Downgraded or encapsulated if (writer == null) { writer = new ArrayWriter(); writer.write(bytes, 0, headerNameStart); } else { writer.write(bytes, lastIndex, headerNameStart - lastIndex); } WordWrapEncoder encoder = null; if (status[0] == 2) { encoder = new WordWrapEncoder((origRecipient ? "Downgraded-Original-Recipient" : "Downgraded-Final-Recipient" ) + ":"); } else { encoder = new WordWrapEncoder(origFieldName); } encoder.AddString(headerValue); byte[] newBytes = DataUtilities.GetUtf8Bytes( encoder.toString(), true); writer.write(newBytes, 0, newBytes.length); lastIndex = headerValueEnd; } } } if (writer != null) { writer.write(bytes, lastIndex, bytes.length - lastIndex); bytes = writer.ToArray(); } return bytes; } private static void ReadHeaders( IByteReader stream, Collection headerList, boolean start) { int lineCount = 0; int[] bytesRead = new int[1]; StringBuilder sb = new StringBuilder(); TransformWithUnget ungetStream = new TransformWithUnget(stream); while (true) { sb.delete(0, (0)+(sb.length())); boolean first = true; boolean endOfHeaders = false; boolean wsp = false; lineCount = 0; while (true) { int c = ungetStream.read(); if (c == -1) { throw new MessageDataException("Premature end before all headers were read"); } ++lineCount; if (first && c == '\r') { if (ungetStream.read() == '\n') { endOfHeaders = true; break; } throw new MessageDataException("CR not followed by LF"); } if ((c >= 0x21 && c <= 57) || (c >= 59 && c <= 0x7e)) { if (wsp) { throw new MessageDataException("Whitespace within header field name"); } first = false; if (c >= 'A' && c <= 'Z') { c += 0x20; } sb.append((char)c); } else if (!first && c == ':') { if (lineCount > 997) { // 998 characters includes the colon and whitespace throw new MessageDataException("Header field name too long"); } break; } else if (c == 0x20 || c == 0x09) { if (start && c == 0x20 && sb.length() == 4 && sb.toString().equals( "From")) { // Mbox convention, skip the entire line sb.delete(0, (0)+(sb.length())); while (true) { c = ungetStream.read(); if (c == -1) { throw new MessageDataException("Premature end before all headers were read"); } if (c == '\r') { if (ungetStream.read() == '\n') { // End of line was reached break; } ungetStream.Unget(); } } start = false; wsp = false; first = true; } else { wsp = true; first = false; } } else { throw new MessageDataException("Malformed header field name"); } } if (endOfHeaders) { break; } if (sb.length() == 0) { throw new MessageDataException("Empty header field name"); } // Set the header field name to the // String builder's current value String fieldName = sb.toString(); // Clear the String builder to read the // header field's value sb.delete(0, (0)+(sb.length())); // Read the header field value using UTF-8 characters // rather than bytes (DEVIATION: RFC 6532 allows UTF-8 // in header field values, but not everywhere in these values, // as is done here for convenience) while (true) { int c = ReadUtf8Char(ungetStream, bytesRead); if (c == -1) { throw new MessageDataException( "Premature end before all headers were read"); } if (c == '\r') { // We're only looking for the single-byte LF, so // there's no need to use ReadUtf8Char c = ungetStream.read(); if (c == '\n') { lineCount = 0; // Parse obsolete folding whitespace (obs-fws) under RFC5322 // (parsed according to errata), same as LWSP in RFC5234 boolean fwsFirst = true; boolean haveFWS = false; while (true) { // Skip the CRLF pair, if any (except if iterating for // the first time, since CRLF was already parsed) // Use ReadByte here since we're just looking for the single // byte characters CR and LF if (!fwsFirst) { c = ungetStream.read(); if (c == '\r') { c = ungetStream.read(); if (c == '\n') { // CRLF was read lineCount = 0; } else { // It's the first part of the line, where the header name // should be, so the CR here is illegal throw new MessageDataException("CR not followed by LF"); } } else { // anything else, unget ungetStream.Unget(); } } fwsFirst = false; // Use ReadByte here since we're just looking for the single // byte characters space and tab int c2 = ungetStream.read(); if (c2 == 0x20 || c2 == 0x09) { ++lineCount; // Don't write SPACE as the first character of the value if (c2 != 0x20 || sb.length() != 0) { sb.append((char)c2); } haveFWS = true; } else { ungetStream.Unget(); // this isn't space or tab; if this is the staart // of the line, this is no longer FWS if (lineCount == 0) { haveFWS = false; } break; } } if (haveFWS) { // We have folding whitespace, line // count found as above continue; } // This ends the header field break; } if (c < 0) { throw new MessageDataException("Premature end before all headers were read"); } sb.append('\r'); ungetStream.Unget(); ++lineCount; } lineCount += bytesRead[0]; // NOTE: Header field line limit not enforced here, only // in the header field name; it's impossible to generate // a conforming message if the name is too long // NOTE: Some emails still have 8-bit bytes in an unencoded // subject line // or other unstructured header field; however, since RFC6532, // we can just assume the UTF-8 encoding in these cases; in // case the bytes are not valid UTF-8, a replacement character // will be output if (c != 0x20 || sb.length() != 0) { if (c <= 0xffff) { sb.append((char)c); } else if (c <= 0x10ffff) { sb.append((char)((((c - 0x10000) >> 10) & 0x3ff) + 0xd800)); sb.append((char)(((c - 0x10000) & 0x3ff) + 0xdc00)); } } } String fieldValue = sb.toString(); headerList.add(fieldName); headerList.add(fieldValue); } } private static class MessageStackEntry { private final Message message; /** * Gets an internal value. * @return An internal value. */ public final Message getMessage() { return this.message; } private final String boundary; /** * Gets an internal value. * @return An internal value. */ public final String getBoundary() { return this.boundary; } public MessageStackEntry (Message msg) { this.message = msg; String newBoundary = ""; MediaType mediaType = msg.getContentType(); if (mediaType.isMultipart()) { newBoundary = mediaType.GetParameter("boundary"); if (newBoundary == null) { throw new MessageDataException("Multipart message has no boundary defined"); } if (!IsWellFormedBoundary(newBoundary)) { throw new MessageDataException("Multipart message has an invalid boundary defined: " + newBoundary); } } this.boundary = newBoundary; } } private void ReadMultipartBody(IByteReader stream) { int baseTransferEncoding = this.transferEncoding; BoundaryCheckerTransform boundaryChecker = new BoundaryCheckerTransform(stream); // Be liberal on the preamble and epilogue of multipart // messages, as they will be ignored. IByteReader currentTransform = MakeTransferEncoding( boundaryChecker, baseTransferEncoding, true); List multipartStack = new ArrayList(); MessageStackEntry entry = new Message.MessageStackEntry(this); multipartStack.add(entry); boundaryChecker.PushBoundary(entry.getBoundary()); Message leaf = null; byte[] buffer = new byte[8192]; int bufferCount = 0; int bufferLength = buffer.length; this.body = new byte[0]; java.io.ByteArrayOutputStream ms = null; try { ms = new java.io.ByteArrayOutputStream(); while (true) { int ch = 0; try { ch = currentTransform.read(); } catch (MessageDataException ex) { String valueExMessage = ex.getMessage(); throw new MessageDataException(valueExMessage); } if (ch < 0) { if (boundaryChecker.getHasNewBodyPart()) { Message msg = new Message(); int stackCount = boundaryChecker.BoundaryCount(); // Pop entries if needed to match the stack if (leaf != null) { if (bufferCount > 0) { ms.write(buffer, 0, bufferCount); } leaf.body = ms.toByteArray(); // Clear for the next body ms.reset(); bufferCount = 0; } else { // Clear for the next body bufferCount = 0; ms.reset(); } while (multipartStack.size() > stackCount) { multipartStack.remove(stackCount); } Message parentMessage = multipartStack.get(multipartStack.size() - 1).getMessage(); boundaryChecker.StartBodyPartHeaders(); MediaType ctype = parentMessage.getContentType(); boolean parentIsDigest = ctype.getSubType().equals("digest") && ctype.isMultipart(); ReadHeaders(stream, msg.headers, false); msg.ProcessHeaders(true, parentIsDigest); entry = new MessageStackEntry(msg); // Add the body part to the multipart // message's list of parts parentMessage.getParts().add(msg); multipartStack.add(entry); ms.reset(); ctype = msg.getContentType(); leaf = ctype.isMultipart() ? null : msg; boundaryChecker.PushBoundary(entry.getBoundary()); boundaryChecker.EndBodyPartHeaders(); currentTransform = MakeTransferEncoding( boundaryChecker, msg.transferEncoding, ctype.getTypeAndSubType().equals("text/plain")); } else { // All body parts were read if (leaf != null) { if (bufferCount > 0) { ms.write(buffer, 0, bufferCount); bufferCount = 0; } leaf.body = ms.toByteArray(); } return; } } else { buffer[bufferCount++] = (byte)ch; if (bufferCount >= bufferLength) { ms.write(buffer, 0, bufferCount); bufferCount = 0; } } } } finally { try { if (ms != null)ms.close(); } catch (java.io.IOException ex) {} } } private static IByteReader MakeTransferEncoding( IByteReader stream, int encoding, boolean useLiberalSevenBit) { IByteReader transform = new EightBitTransform(stream); if (encoding == EncodingQuotedPrintable) { // NOTE: The max line size is actually 76, but some emails // have lines that exceed this size, so use an unlimited line length // when parsing transform = new QuotedPrintableTransform(stream, false, -1); // transform = new QuotedPrintableTransform(stream, false, 76, true); } else if (encoding == EncodingBase64) { // NOTE: Same as quoted-printable regarding line length transform = new Base64Transform(stream, false, -1, false); // transform = new Base64Transform(stream, false, 76, true); } else if (encoding == EncodingEightBit) { transform = new EightBitTransform(stream); } else if (encoding == EncodingBinary) { transform = stream; } else if (encoding == EncodingSevenBit) { // DEVIATION: Replace 8-bit bytes and null with the // ASCII substitute character (0x1a) for text/plain messages, // non-MIME messages, and the preamble and epilogue of multipart // messages (which will be ignored). transform = useLiberalSevenBit ? ((IByteReader)new LiberalSevenBitTransform(stream)) : ((IByteReader)new SevenBitTransform(stream)); } return transform; } private void ReadSimpleBody(IByteReader stream) { IByteReader transform = MakeTransferEncoding( stream, this.transferEncoding, this.getContentType().getTypeAndSubType().equals("text/plain")); byte[] buffer = new byte[8192]; int bufferCount = 0; int bufferLength = buffer.length; java.io.ByteArrayOutputStream ms = null; try { ms = new java.io.ByteArrayOutputStream(); while (true) { int ch = 0; try { ch = transform.read(); } catch (MessageDataException ex) { String valueExMessage = ex.getMessage(); throw new MessageDataException(valueExMessage, ex); } if (ch < 0) { break; } buffer[bufferCount++] = (byte)ch; if (bufferCount >= bufferLength) { ms.write(buffer, 0, bufferCount); bufferCount = 0; } } if (bufferCount > 0) { ms.write(buffer, 0, bufferCount); } this.body = ms.toByteArray(); } finally { try { if (ms != null)ms.close(); } catch (java.io.IOException ex) {} } } private void ReadMessage(IByteReader stream) { try { ReadHeaders(stream, this.headers, true); this.ProcessHeaders(false, false); if (this.contentType.isMultipart()) { this.ReadMultipartBody(stream); } else { this.ReadSimpleBody(stream); } } catch (IllegalStateException ex) { throw new MessageDataException(ex.getMessage(), ex); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy