001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.core;
019:
020: import org.apache.avalon.framework.activity.Disposable;
021: import org.apache.avalon.framework.container.ContainerUtil;
022: import org.apache.james.util.InternetPrintWriter;
023: import org.apache.james.util.io.IOUtil;
024:
025: import javax.activation.DataHandler;
026: import javax.mail.MessagingException;
027: import javax.mail.Session;
028: import javax.mail.internet.InternetHeaders;
029: import javax.mail.internet.MimeMessage;
030: import javax.mail.util.SharedByteArrayInputStream;
031:
032: import java.io.BufferedWriter;
033: import java.io.ByteArrayOutputStream;
034: import java.io.IOException;
035: import java.io.InputStream;
036: import java.io.InputStreamReader;
037: import java.io.LineNumberReader;
038: import java.io.OutputStream;
039: import java.io.OutputStreamWriter;
040: import java.io.PrintWriter;
041: import java.util.Enumeration;
042:
043: /**
044: * This object wraps a MimeMessage, only loading the underlying MimeMessage
045: * object when needed. Also tracks if changes were made to reduce
046: * unnecessary saves.
047: */
048: public class MimeMessageWrapper extends MimeMessage implements
049: Disposable {
050:
051: /**
052: * Can provide an input stream to the data
053: */
054: protected MimeMessageSource source = null;
055:
056: /**
057: * This is false until we parse the message
058: */
059: protected boolean messageParsed = false;
060:
061: /**
062: * This is false until we parse the message
063: */
064: protected boolean headersModified = false;
065:
066: /**
067: * This is false until we parse the message
068: */
069: protected boolean bodyModified = false;
070:
071: /**
072: * Keep a reference to the sourceIn so we can close it
073: * only when we dispose the message.
074: */
075: private InputStream sourceIn;
076:
077: private MimeMessageWrapper(Session session)
078: throws MessagingException {
079: super (session);
080: this .headers = null;
081: this .modified = false;
082: this .headersModified = false;
083: this .bodyModified = false;
084: }
085:
086: /**
087: * A constructor that instantiates a MimeMessageWrapper based on
088: * a MimeMessageSource
089: *
090: * @param source the MimeMessageSource
091: * @throws MessagingException
092: */
093: public MimeMessageWrapper(Session session, MimeMessageSource source)
094: throws MessagingException {
095: this (session);
096: this .source = source;
097: }
098:
099: /**
100: * A constructor that instantiates a MimeMessageWrapper based on
101: * a MimeMessageSource
102: *
103: * @param source the MimeMessageSource
104: * @throws MessagingException
105: * @throws MessagingException
106: */
107: public MimeMessageWrapper(MimeMessageSource source)
108: throws MessagingException {
109: this (Session.getDefaultInstance(System.getProperties()), source);
110: }
111:
112: public MimeMessageWrapper(MimeMessage original)
113: throws MessagingException {
114: this (Session.getDefaultInstance(System.getProperties()));
115: flags = original.getFlags();
116:
117: // if the original is an unmodified MimeMessageWrapped we clone the headers and
118: // take its source.
119: /* Temporary commented out because of JAMES-474
120: if (original instanceof MimeMessageWrapper && !((MimeMessageWrapper) original).bodyModified) {
121: source = ((MimeMessageWrapper) original).source;
122: // this probably speed up things
123: if (((MimeMessageWrapper) original).headers != null) {
124: ByteArrayOutputStream temp = new ByteArrayOutputStream();
125: InternetHeaders ih = ((MimeMessageWrapper) original).headers;
126: MimeMessageUtil.writeHeadersTo(ih.getAllHeaderLines(),temp);
127: headers = createInternetHeaders(new ByteArrayInputStream(temp.toByteArray()));
128: headersModified = ((MimeMessageWrapper) original).headersModified;
129: }
130: }
131: */
132:
133: if (source == null) {
134: ByteArrayOutputStream bos;
135: int size = original.getSize();
136: if (size > 0)
137: bos = new ByteArrayOutputStream(size);
138: else
139: bos = new ByteArrayOutputStream();
140: try {
141: original.writeTo(bos);
142: bos.close();
143: SharedByteArrayInputStream bis = new SharedByteArrayInputStream(
144: bos.toByteArray());
145: parse(bis);
146: bis.close();
147: saved = true;
148: } catch (IOException ex) {
149: // should never happen, but just in case...
150: throw new MessagingException(
151: "IOException while copying message", ex);
152: }
153: }
154: }
155:
156: /**
157: * Returns the source ID of the MimeMessageSource that is supplying this
158: * with data.
159: * @see MimeMessageSource
160: */
161: public synchronized String getSourceId() {
162: return source != null ? source.getSourceId() : null;
163: }
164:
165: /**
166: * Load the message headers from the internal source.
167: *
168: * @throws MessagingException if an error is encountered while
169: * loading the headers
170: */
171: protected synchronized void loadHeaders() throws MessagingException {
172: if (headers != null) {
173: //Another thread has already loaded these headers
174: return;
175: } else if (source != null) {
176: try {
177: InputStream in = source.getInputStream();
178: try {
179: headers = createInternetHeaders(in);
180: } finally {
181: IOUtil.shutdownStream(in);
182: }
183: } catch (IOException ioe) {
184: throw new MessagingException(
185: "Unable to parse headers from stream: "
186: + ioe.getMessage(), ioe);
187: }
188: } else {
189: throw new MessagingException(
190: "loadHeaders called for a message with no source, contentStream or stream");
191: }
192: }
193:
194: /**
195: * Load the complete MimeMessage from the internal source.
196: *
197: * @throws MessagingException if an error is encountered while
198: * loading the message
199: */
200: protected synchronized void loadMessage() throws MessagingException {
201: if (messageParsed) {
202: //Another thread has already loaded this message
203: return;
204: } else if (source != null) {
205: sourceIn = null;
206: try {
207: sourceIn = source.getInputStream();
208:
209: parse(sourceIn);
210: // TODO is it ok?
211: saved = true;
212:
213: } catch (IOException ioe) {
214: IOUtil.shutdownStream(sourceIn);
215: sourceIn = null;
216: throw new MessagingException("Unable to parse stream: "
217: + ioe.getMessage(), ioe);
218: }
219: } else {
220: throw new MessagingException(
221: "loadHeaders called for an unparsed message with no source");
222: }
223: }
224:
225: /**
226: * Get whether the message has been modified.
227: *
228: * @return whether the message has been modified
229: */
230: public synchronized boolean isModified() {
231: return headersModified || bodyModified || modified;
232: }
233:
234: /**
235: * Rewritten for optimization purposes
236: */
237: public synchronized void writeTo(OutputStream os)
238: throws IOException, MessagingException {
239: if (source != null && !isModified()) {
240: // We do not want to instantiate the message... just read from source
241: // and write to this outputstream
242: InputStream in = source.getInputStream();
243: try {
244: MimeMessageUtil.copyStream(in, os);
245: } finally {
246: IOUtil.shutdownStream(in);
247: }
248: } else {
249: writeTo(os, os);
250: }
251: }
252:
253: /**
254: * Rewritten for optimization purposes
255: */
256: public void writeTo(OutputStream os, String[] ignoreList)
257: throws IOException, MessagingException {
258: writeTo(os, os, ignoreList);
259: }
260:
261: /**
262: * Write
263: */
264: public void writeTo(OutputStream headerOs, OutputStream bodyOs)
265: throws IOException, MessagingException {
266: writeTo(headerOs, bodyOs, new String[0]);
267: }
268:
269: public synchronized void writeTo(OutputStream headerOs,
270: OutputStream bodyOs, String[] ignoreList)
271: throws IOException, MessagingException {
272: if (source != null && !isModified()) {
273: //We do not want to instantiate the message... just read from source
274: // and write to this outputstream
275:
276: //First handle the headers
277: InputStream in = source.getInputStream();
278: try {
279: InternetHeaders headers = new InternetHeaders(in);
280: PrintWriter pos = new InternetPrintWriter(
281: new BufferedWriter(new OutputStreamWriter(
282: headerOs), 512), true);
283: for (Enumeration e = headers
284: .getNonMatchingHeaderLines(ignoreList); e
285: .hasMoreElements();) {
286: String header = (String) e.nextElement();
287: pos.println(header);
288: }
289: pos.println();
290: pos.flush();
291: MimeMessageUtil.copyStream(in, bodyOs);
292: } finally {
293: IOUtil.shutdownStream(in);
294: }
295: } else {
296: MimeMessageUtil.writeToInternal(this , headerOs, bodyOs,
297: ignoreList);
298: }
299: }
300:
301: /**
302: * This is the MimeMessage implementation - this should return ONLY the
303: * body, not the entire message (should not count headers). Will have
304: * to parse the message.
305: */
306: public int getSize() throws MessagingException {
307: if (!messageParsed) {
308: loadMessage();
309: }
310: return super .getSize();
311: }
312:
313: /**
314: * Corrects JavaMail 1.1 version which always returns -1.
315: * Only corrected for content less than 5000 bytes,
316: * to avoid memory hogging.
317: */
318: public int getLineCount() throws MessagingException {
319: InputStream in = null;
320: try {
321: in = getContentStream();
322: } catch (Exception e) {
323: return -1;
324: }
325: if (in == null) {
326: return -1;
327: }
328: //Wrap input stream in LineNumberReader
329: //Not sure what encoding to use really...
330: try {
331: LineNumberReader counter;
332: if (getEncoding() != null) {
333: counter = new LineNumberReader(new InputStreamReader(
334: in, getEncoding()));
335: } else {
336: counter = new LineNumberReader(
337: new InputStreamReader(in));
338: }
339: //Read through all the data
340: char[] block = new char[4096];
341: while (counter.read(block) > -1) {
342: //Just keep reading
343: }
344: return counter.getLineNumber();
345: } catch (IOException ioe) {
346: return -1;
347: } finally {
348: IOUtil.shutdownStream(in);
349: }
350: }
351:
352: /**
353: * Returns size of message, ie headers and content
354: */
355: public long getMessageSize() throws MessagingException {
356: if (source != null && !isModified()) {
357: try {
358: return source.getMessageSize();
359: } catch (IOException ioe) {
360: throw new MessagingException(
361: "Error retrieving message size", ioe);
362: }
363: } else {
364: return MimeMessageUtil.calculateMessageSize(this );
365: }
366: }
367:
368: /**
369: * We override all the "headers" access methods to be sure that we
370: * loaded the headers
371: */
372:
373: public String[] getHeader(String name) throws MessagingException {
374: if (headers == null) {
375: loadHeaders();
376: }
377: return headers.getHeader(name);
378: }
379:
380: public String getHeader(String name, String delimiter)
381: throws MessagingException {
382: if (headers == null) {
383: loadHeaders();
384: }
385: return headers.getHeader(name, delimiter);
386: }
387:
388: public Enumeration getAllHeaders() throws MessagingException {
389: if (headers == null) {
390: loadHeaders();
391: }
392: return headers.getAllHeaders();
393: }
394:
395: public Enumeration getMatchingHeaders(String[] names)
396: throws MessagingException {
397: if (headers == null) {
398: loadHeaders();
399: }
400: return headers.getMatchingHeaders(names);
401: }
402:
403: public Enumeration getNonMatchingHeaders(String[] names)
404: throws MessagingException {
405: if (headers == null) {
406: loadHeaders();
407: }
408: return headers.getNonMatchingHeaders(names);
409: }
410:
411: public Enumeration getAllHeaderLines() throws MessagingException {
412: if (headers == null) {
413: loadHeaders();
414: }
415: return headers.getAllHeaderLines();
416: }
417:
418: public Enumeration getMatchingHeaderLines(String[] names)
419: throws MessagingException {
420: if (headers == null) {
421: loadHeaders();
422: }
423: return headers.getMatchingHeaderLines(names);
424: }
425:
426: public Enumeration getNonMatchingHeaderLines(String[] names)
427: throws MessagingException {
428: if (headers == null) {
429: loadHeaders();
430: }
431: return headers.getNonMatchingHeaderLines(names);
432: }
433:
434: private synchronized void checkModifyHeaders()
435: throws MessagingException {
436: // Disable only-header loading optimizations for JAMES-559
437: if (!messageParsed) {
438: loadMessage();
439: }
440: // End JAMES-559
441: if (headers == null) {
442: loadHeaders();
443: }
444: modified = true;
445: saved = false;
446: headersModified = true;
447: }
448:
449: public void setHeader(String name, String value)
450: throws MessagingException {
451: checkModifyHeaders();
452: super .setHeader(name, value);
453: }
454:
455: public void addHeader(String name, String value)
456: throws MessagingException {
457: checkModifyHeaders();
458: super .addHeader(name, value);
459: }
460:
461: public void removeHeader(String name) throws MessagingException {
462: checkModifyHeaders();
463: super .removeHeader(name);
464: }
465:
466: public void addHeaderLine(String line) throws MessagingException {
467: checkModifyHeaders();
468: super .addHeaderLine(line);
469: }
470:
471: /**
472: * The message is changed when working with headers and when altering the content.
473: * Every method that alter the content will fallback to this one.
474: *
475: * @see javax.mail.Part#setDataHandler(javax.activation.DataHandler)
476: */
477: public synchronized void setDataHandler(DataHandler arg0)
478: throws MessagingException {
479: modified = true;
480: saved = false;
481: bodyModified = true;
482: super .setDataHandler(arg0);
483: }
484:
485: /**
486: * @see org.apache.avalon.framework.activity.Disposable#dispose()
487: */
488: public void dispose() {
489: if (sourceIn != null) {
490: IOUtil.shutdownStream(sourceIn);
491: }
492: if (source != null) {
493: ContainerUtil.dispose(source);
494: }
495: }
496:
497: /**
498: * @see javax.mail.internet.MimeMessage#parse(java.io.InputStream)
499: */
500: protected synchronized void parse(InputStream is)
501: throws MessagingException {
502: // the super implementation calls
503: // headers = createInternetHeaders(is);
504: super .parse(is);
505: messageParsed = true;
506: }
507:
508: /**
509: * If we already parsed the headers then we simply return the updated ones.
510: * Otherwise we parse
511: *
512: * @see javax.mail.internet.MimeMessage#createInternetHeaders(java.io.InputStream)
513: */
514: protected synchronized InternetHeaders createInternetHeaders(
515: InputStream is) throws MessagingException {
516: /* This code is no more needed: see JAMES-570 and new tests
517:
518: * InternetHeaders can be a bit awkward to work with due to
519: * its own internal handling of header order. This hack may
520: * not always be necessary, but for now we are trying to
521: * ensure that there is a Return-Path header, even if just a
522: * placeholder, so that later, e.g., in LocalDelivery, when we
523: * call setHeader, it will remove any other Return-Path
524: * headers, and ensure that ours is on the top. addHeader
525: * handles header order, but not setHeader. This may change in
526: * future JavaMail. But if there are other Return-Path header
527: * values, let's drop our placeholder.
528:
529: MailHeaders newHeaders = new MailHeaders(new ByteArrayInputStream((RFC2822Headers.RETURN_PATH + ": placeholder").getBytes()));
530: newHeaders.setHeader(RFC2822Headers.RETURN_PATH, null);
531: newHeaders.load(is);
532: String[] returnPathHeaders = newHeaders.getHeader(RFC2822Headers.RETURN_PATH);
533: if (returnPathHeaders.length > 1) newHeaders.setHeader(RFC2822Headers.RETURN_PATH, returnPathHeaders[1]);
534: */
535:
536: // Keep this: skip the headers from the stream
537: // we could put that code in the else and simple write an "header" skipping
538: // reader for the others.
539: MailHeaders newHeaders = new MailHeaders(is);
540:
541: if (headers != null) {
542: return headers;
543: } else {
544: return newHeaders;
545: }
546: }
547:
548: /**
549: * @see javax.mail.internet.MimeMessage#getContentStream()
550: */
551: protected InputStream getContentStream() throws MessagingException {
552: if (!messageParsed) {
553: loadMessage();
554: }
555: return super .getContentStream();
556: }
557:
558: /**
559: * @see javax.mail.internet.MimeMessage#getRawInputStream()
560: */
561: public InputStream getRawInputStream() throws MessagingException {
562: if (!messageParsed && !isModified() && source != null) {
563: InputStream is;
564: try {
565: is = source.getInputStream();
566: // skip the headers.
567: new MailHeaders(is);
568: return is;
569: } catch (IOException e) {
570: throw new MessagingException(
571: "Unable to read the stream: " + e.getMessage(),
572: e);
573: }
574: } else
575: return super.getRawInputStream();
576: }
577:
578: }
|