001: package com.protomatter.util;
002:
003: /**
004: * {{{ The Protomatter Software License, Version 1.0
005: * derived from The Apache Software License, Version 1.1
006: *
007: * Copyright (c) 1998-2002 Nate Sammons. All rights reserved.
008: *
009: * Redistribution and use in source and binary forms, with or without
010: * modification, are permitted provided that the following conditions
011: * are met:
012: *
013: * 1. Redistributions of source code must retain the above copyright
014: * notice, this list of conditions and the following disclaimer.
015: *
016: * 2. Redistributions in binary form must reproduce the above copyright
017: * notice, this list of conditions and the following disclaimer in
018: * the documentation and/or other materials provided with the
019: * distribution.
020: *
021: * 3. The end-user documentation included with the redistribution,
022: * if any, must include the following acknowledgment:
023: * "This product includes software developed for the
024: * Protomatter Software Project
025: * (http://protomatter.sourceforge.net/)."
026: * Alternately, this acknowledgment may appear in the software itself,
027: * if and wherever such third-party acknowledgments normally appear.
028: *
029: * 4. The names "Protomatter" and "Protomatter Software Project" must
030: * not be used to endorse or promote products derived from this
031: * software without prior written permission. For written
032: * permission, please contact support@protomatter.com.
033: *
034: * 5. Products derived from this software may not be called "Protomatter",
035: * nor may "Protomatter" appear in their name, without prior written
036: * permission of the Protomatter Software Project
037: * (support@protomatter.com).
038: *
039: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
040: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
041: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
042: * DISCLAIMED. IN NO EVENT SHALL THE PROTOMATTER SOFTWARE PROJECT OR
043: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
044: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
045: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
046: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
047: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
048: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
049: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
050: * SUCH DAMAGE. }}}
051: */
052:
053: import java.io.*;
054: import java.util.*;
055: import java.text.*;
056:
057: /**
058: * A MIME encoded message.
059: * This is basically a collection of MIMEAttachment objects. This
060: * class takes care of the ASCII encoding of the message as a whole,
061: * including the segment boundary, etc... It does <b><i>NOT</i></b>
062: * take care of any headers other than the Content-Type, which it
063: * always identifies as "MULTIPART/MIXED".
064: * This class can also be used to parse "file upload" information
065: * out of HTML forms.
066: *
067: * @see MIMEAttachment
068: */
069: public class MIMEMessage implements Serializable {
070: private Vector attachments;
071: private String boundary;
072: private static String CRLF = "\r\n";
073:
074: /**
075: * Initialize the MIMEMessage.
076: */
077: public MIMEMessage() {
078: attachments = new Vector();
079: boundary = "--------------74329329-84328432-279-4382"; // some gibberish
080: }
081:
082: /**
083: * Get the Content-Type of this message, also includes the boundary.
084: */
085: public String getContentType() {
086: return "MULTIPART/MIXED; BOUNDARY=\"" + boundary + "\"";
087: }
088:
089: /**
090: * Add an attachment to this message
091: */
092: public void addAttachment(MIMEAttachment a) {
093: attachments.addElement(a);
094: }
095:
096: /**
097: * Remove an attachment to this message
098: */
099: public void removeAttachment(MIMEAttachment a) {
100: attachments.removeElement(a);
101: }
102:
103: /**
104: * Get an enumeration of the attachments to this message.
105: */
106: public Enumeration getAttachments() {
107: return attachments.elements();
108: }
109:
110: /**
111: * Get the boundary between parts of the message.
112: */
113: public String getBoundary() {
114: return boundary;
115: }
116:
117: /**
118: * Set the boundary between parts of the message.
119: */
120: public void setBoundary(String boundary) {
121: this .boundary = boundary;
122: }
123:
124: /**
125: * Return the encoded message (including all attachments)
126: */
127: public String toString() {
128: StringWriter sw = new StringWriter();
129: PrintWriter pw = new PrintWriter(sw);
130: write(pw);
131: pw.flush();
132: return sw.toString();
133: }
134:
135: /**
136: * Write this message to the given output stream.
137: */
138: public void write(PrintWriter w) {
139: Enumeration e = getAttachments();
140: while (e.hasMoreElements()) {
141: MIMEAttachment a = (MIMEAttachment) e.nextElement();
142: w.print("--");
143: w.print(boundary);
144: w.print(CRLF);
145: a.write(w);
146: w.print(CRLF);
147: }
148: w.print("--");
149: w.print(boundary);
150: w.print("--");
151: w.print(CRLF);
152: }
153:
154: /**
155: * Return a MIMEMessage built from the InputStream that
156: * points to a MIME message. Reads the stream fully
157: * before parsing, so watch out.
158: */
159: public static MIMEMessage parse(InputStream s) throws MIMEException {
160: byte[] data = null;
161: try {
162: data = readInputStreamFully(s);
163: } catch (Exception x) {
164: throw new MIMEException(
165: MessageFormat
166: .format(
167: UtilResources
168: .getResourceString(MessageConstants.MIME_EXCEPTION_IN_INPUT),
169: new Object[] { x.toString() }));
170: }
171: return parse(data);
172: }
173:
174: /**
175: * Return a MIMEMessage built from the data.
176: */
177: public static MIMEMessage parse(byte data[]) throws MIMEException {
178: try {
179: MIMEMessage message = new MIMEMessage();
180:
181: // set up a vector for passing a second argument back out of methods.
182: // v holds the new current index after calls to readLine() and
183: // readBody()
184: int index = 0;
185: int endIndex = data.length - 1;
186:
187: // MSIE 4.0Bsomething puts in extra whitespace at the front of
188: // the file, which does not conform to the specification!
189: // In the immortal words of Charlton Heston "Damn You! Damn You!"
190: // also clip whitespace at the end of the message.
191: try {
192: while (Character.isWhitespace((char) data[index]))
193: ++index;
194: while (Character.isWhitespace((char) data[endIndex]))
195: --endIndex;
196: endIndex++;
197: } catch (Exception x) {
198: throw new MIMEException(
199: UtilResources
200: .getResourceString(MessageConstants.MIME_ALL_WHITESPACE));
201: }
202:
203: // this vector contains the begin and end indexes of the data. I didn't
204: // truncate the data, since it's a waste of time and memory.
205: Vector v = new Vector(2);
206: v.addElement(new Integer(index));
207: v.addElement(new Integer(endIndex));
208:
209: // first line is the separator
210: String sep = null;
211: try {
212: sep = readLine(data, v);
213: } catch (Exception x) {
214: throw new MIMEException(
215: MessageFormat
216: .format(
217: UtilResources
218: .getResourceString(MessageConstants.MIME_EXCEPTION_IN_SEPARATOR),
219: new Object[] { x.toString() }));
220: }
221: if (sep == null) {
222: throw new MIMEException(
223: UtilResources
224: .getResourceString(MessageConstants.MIME_SEPARATOR_NOT_FOUND));
225: }
226:
227: try {
228: while (index < endIndex) {
229: // read headers
230: String line = "x";
231: Hashtable headers = new Hashtable();
232: line = readLine(data, v);
233: while (!line.equals("")) // headers are separated from body by a blank line
234: {
235: // header looks like "name: value"
236: int cIndex = line.indexOf(":");
237: if (cIndex != -1) // dodge MSIE lameness.
238: {
239: headers.put(line.substring(0, cIndex), line
240: .substring(cIndex + 2));
241: }
242: line = readLine(data, v);
243: }
244:
245: // read content
246: StringBuffer info = new StringBuffer(); // either "ascii" or "binary"
247: byte[] content = readBody(sep, data, info, v);
248:
249: // add the attachment to the list.
250: if (content != null) {
251: MIMEAttachment a = new MIMEAttachment();
252: //a.setBinary(info.toString().equals("binary"));
253: a.setHeaders(headers); // set headers in the attachment.
254: String encoding = a
255: .getHeader("Content-Transfer-Encoding");
256: if (encoding != null
257: && encoding.equalsIgnoreCase("BASE64")) {
258: byte[] c = Base64
259: .decode(removeWhitespace(content));
260: a.setContent(c);
261: a.setBinary(isBinaryContent(c));
262: } else {
263: a.setContent(content);
264: a.setBinary(isBinaryContent(content));
265: }
266: message.addAttachment(a); // now add the attachment.
267: } else {
268: return message;
269: }
270: index = getIndex(v);
271: }
272: } catch (Exception x) {
273: ; // done reading... not a problem.
274: }
275: return message;
276: } catch (Exception x) {
277: throw new MIMEException(
278: MessageFormat
279: .format(
280: UtilResources
281: .getResourceString(MessageConstants.MIME_EXCEPTION_IN_PARSE),
282: new Object[] { x.toString() }));
283: }
284: }
285:
286: // these should get inlined when -O is used to compile
287: private static final int getIndex(Vector v) {
288: return ((Integer) v.firstElement()).intValue();
289: }
290:
291: private static final int getEndIndex(Vector v) {
292: return ((Integer) v.elementAt(1)).intValue();
293: }
294:
295: private static final void setIndex(Vector v, int i) {
296: v.setElementAt(new Integer(i), 0);
297: }
298:
299: private static final void setEndIndex(Vector v, int i) {
300: v.setElementAt(new Integer(i), 1);
301: }
302:
303: // reads a line of text. The end can be any of:
304: // CR, LF, CRLF Sets index in v to be the first char
305: // *AFTER* the end of the line marker. Text returned does
306: // *NOT* include the end of line marker.
307: private final static String readLine(byte[] data, Vector v)
308: throws Exception {
309: int index = getIndex(v);
310: int endIndex = getEndIndex(v);
311: if (index == endIndex)
312: return new String();
313: int c;
314: ByteArrayOutputStream b = new ByteArrayOutputStream();
315: while (index < endIndex) {
316: c = (int) data[index];
317: if (isLF(c)) // "\n"
318: {
319: index++;
320: setIndex(v, index);
321: return new String(b.toByteArray());
322: }
323: if (isCR(c)) // "\r"
324: {
325: index++;
326: if (isLF((int) data[index]))
327: ++index;
328: setIndex(v, index);
329: return new String(b.toByteArray());
330: }
331: b.write(c);
332: index++;
333: }
334: setIndex(v, index);
335: return new String(b.toByteArray());
336: }
337:
338: /**
339: * Scan the content and decide if it's binary or ASCII data.
340: */
341: public static boolean isBinaryContent(byte[] data) {
342: return isBinaryContent(data, 0, data.length);
343: }
344:
345: /**
346: * Scan the content and decide if it's binary or ASCII data.
347: */
348: public static boolean isBinaryContent(byte[] data, int start,
349: int len) {
350: byte[] d = data;
351: for (int i = start; i < len; i++) {
352: if (((int) d[i]) < 0)
353: return true;
354: }
355: return false;
356: }
357:
358: //
359: // read until, and including, the separator. Sets index in v to be the first
360: // char after any CRLF action after the separator.
361: //
362: // reads:
363: // binary data<CRLF>
364: // SEPARATOR<CRLF>
365: //
366: // or
367: //
368: // binary data<CRLF>
369: // SEPARATOR--<CRLF>
370: //
371: private final static byte[] readBody(String sep, byte data[],
372: StringBuffer info, Vector v) throws MIMEException {
373: int index = getIndex(v);
374: int endIndex = getEndIndex(v);
375: int sepLen = sep.length();
376: ByteArrayOutputStream buffer = new ByteArrayOutputStream();
377:
378: // assume ascii
379: boolean isBinary = false;
380: info.insert(0, "ascii");
381: info.setLength(5);
382:
383: while (index < endIndex) {
384: // check to see if we're reading binary or ascii
385: if ((int) data[index] < 0 && !isBinary) // binary (only do this once)
386: {
387: info.insert(0, "binary");
388: info.setLength(6);
389: isBinary = true;
390: }
391:
392: // if we get a CRLF, check some stuff.
393: if (isCR((int) data[index]) && isLF((int) data[index + 1])
394: || isLF((int) data[index])
395: && !isCRLF((int) data[index + 1])) {
396: int skip = 0;
397: if (isLF((int) data[index + 1]))
398: skip = 2;
399: else
400: skip = 1;
401:
402: // look ahead and see if the separator is coming up
403: //
404: // index
405: // vv <- sep ->
406: // [CR][LF][?][?][?][END] // MSIE end (incorrect)
407: // [CR][LF][?][?][?][-][-][END] // MSIE end (incorrect)
408: // [CR][LF][?][?][?][-][-][CR][LF][END] // netscape end (correct)
409: // [CR][LF][?][?][?][CR][LF] // middle (both)
410: //
411: // Remember that trailing whitespace was remove above.
412: //
413:
414: // sepTry is a string we think might be the separator.
415: String sepTry = null;
416: sepTry = new String(data, index + skip, sepLen);
417:
418: // if the attempt at a separator is indeed the separator...
419: if (sepTry.equals(sep)) {
420: // separator is the end of the data, or
421: // separator is followed by "--" and then the end.
422: if (((index + skip + sepLen) == endIndex)
423: || ((index + skip + sepLen + 2) == endIndex)) {
424: setIndex(v, endIndex);
425: return buffer.toByteArray();
426: }
427:
428: // check if this is not the end of the entire message,
429: // but is the end of the current attachment's body
430: // (separator is followed by CRLF)
431: if (isCR((int) data[skip + index + sepLen])
432: && isLF((int) data[skip + index + sepLen
433: + 1])
434: || isLF((int) data[skip + index + sepLen])
435: && !isCRLF((int) data[skip + index + sepLen
436: + 1])) {
437: // position index after the CRLF action
438: if (isLF((int) data[index + sepLen + 1]))
439: setIndex(v, index + sepLen + 2);
440: else
441: setIndex(v, index + sepLen + 1);
442: return buffer.toByteArray();
443: }
444: sepTry = null;
445: buffer.write(data, index, skip);
446: index += skip;
447: }
448:
449: // separator didn't match... we can fast-forward a bit.
450: // all we do here is write the CRLF to the buffer.
451: else {
452: buffer.write(data, index, skip);
453: index += skip;
454: }
455: } else {
456: buffer.write((int) data[index++]);
457: }
458: }
459:
460: // hit the end -- return null -- should not get here.
461: setIndex(v, index);
462: return buffer.toByteArray();
463: }
464:
465: //
466: // read a stream fully and return it's contents as an
467: // array of bytes.
468: //
469: private static byte[] readInputStreamFully(InputStream is)
470: throws IOException {
471: ByteArrayOutputStream b = new ByteArrayOutputStream();
472: {
473: int i = 0;
474: while ((i = is.read()) != -1)
475: b.write(i);
476: }
477: return b.toByteArray();
478: }
479:
480: // is the given character a CR?
481: private final static boolean isCR(int i) {
482: return (i == 13);
483: }
484:
485: // is the given character a LF?
486: private final static boolean isLF(int i) {
487: return (i == 10);
488: }
489:
490: // is the given character a CR or an LF?
491: private final static boolean isCRLF(int i) {
492: return ((i == 10) || (i == 13));
493: }
494:
495: private final static byte[] removeWhitespace(byte[] data) {
496: byte[] d = data;
497: ByteArrayOutputStream out = new ByteArrayOutputStream();
498: for (int i = 0; i < d.length; i++) {
499: if (!Character.isWhitespace((char) d[i]))
500: out.write(d[i]);
501: }
502: return out.toByteArray();
503: }
504:
505: public static void main(String args[]) {
506: if (args.length == 0) {
507: System.out.println("Usage: MIMEMessage parse filename");
508: System.out
509: .println(" or MIMEMessage create file1..fileN");
510: System.exit(0);
511: }
512:
513: try {
514: String cmd = args[0];
515: if (cmd.equalsIgnoreCase("parse")) {
516: BufferedInputStream in = new BufferedInputStream(
517: new FileInputStream(new File(args[1])));
518: long time = System.currentTimeMillis();
519: MIMEMessage m = MIMEMessage.parse(in);
520: time = System.currentTimeMillis() - time;
521: System.err.println("Parse took " + time + "ms");
522: System.err.println("");
523: Enumeration e = m.getAttachments();
524: while (e.hasMoreElements()) {
525: MIMEAttachment a = (MIMEAttachment) e.nextElement();
526: System.err.println("Attachment:");
527: System.err.println(" Headers:");
528: Enumeration h = a.getHeaderNames();
529: while (h.hasMoreElements()) {
530: String header = (String) h.nextElement();
531: System.err.println(" " + header + ": "
532: + a.getHeader(header));
533: }
534: System.err.println(" Info:");
535: System.err.println(" Content length: "
536: + a.getContent().length);
537: System.err.println(" Binary: "
538: + a.isBinary());
539: System.err.println("");
540: }
541: System.out.println(m);
542: } else {
543: System.err.println("Creating new MIMEMessage");
544: MIMEMessage m = new MIMEMessage();
545: for (int i = 1; i < args.length; i++) {
546: String file = args[i];
547: String type = "unknown";
548:
549: ByteArrayOutputStream bout = new ByteArrayOutputStream();
550: BufferedInputStream in = new BufferedInputStream(
551: new FileInputStream(new File(file)));
552: byte[] buffer = new byte[8192];
553: int read = 0;
554: while ((read = in.read(buffer)) != -1)
555: bout.write(buffer, 0, read);
556:
557: byte[] data = bout.toByteArray();
558: boolean binary = isBinaryContent(data);
559: MIMEAttachment a = new MIMEAttachment(type, file,
560: data, binary);
561: System.err.println("binary = " + binary);
562: m.addAttachment(a);
563: }
564:
565: System.out.println(m);
566: }
567: } catch (Exception x) {
568: x.printStackTrace();
569: }
570: }
571: }
|