View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17  
18  package org.apache.james.core;
19  
20  import org.apache.avalon.framework.activity.Disposable;
21  import org.apache.avalon.framework.container.ContainerUtil;
22  import org.apache.james.util.InternetPrintWriter;
23  import org.apache.james.util.io.IOUtil;
24  
25  import javax.activation.DataHandler;
26  import javax.mail.MessagingException;
27  import javax.mail.Session;
28  import javax.mail.internet.InternetHeaders;
29  import javax.mail.internet.MimeMessage;
30  import javax.mail.util.SharedByteArrayInputStream;
31  
32  import java.io.BufferedWriter;
33  import java.io.ByteArrayOutputStream;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.InputStreamReader;
37  import java.io.LineNumberReader;
38  import java.io.OutputStream;
39  import java.io.OutputStreamWriter;
40  import java.io.PrintWriter;
41  import java.util.Enumeration;
42  
43  /***
44   * This object wraps a MimeMessage, only loading the underlying MimeMessage
45   * object when needed.  Also tracks if changes were made to reduce
46   * unnecessary saves.
47   */
48  public class MimeMessageWrapper
49      extends MimeMessage
50      implements Disposable {
51  
52      /***
53       * Can provide an input stream to the data
54       */
55      protected MimeMessageSource source = null;
56      
57      /***
58       * This is false until we parse the message 
59       */
60      protected boolean messageParsed = false;
61      
62      /***
63       * This is false until we parse the message 
64       */
65      protected boolean headersModified = false;
66      
67      /***
68       * This is false until we parse the message 
69       */
70      protected boolean bodyModified = false;
71  
72      /***
73       * Keep a reference to the sourceIn so we can close it
74       * only when we dispose the message.
75       */
76      private InputStream sourceIn;
77  
78      private MimeMessageWrapper(Session session) throws MessagingException {
79          super(session);
80          this.headers = null;
81          this.modified = false;
82          this.headersModified = false;
83          this.bodyModified = false;
84      }
85      
86      /***
87       * A constructor that instantiates a MimeMessageWrapper based on
88       * a MimeMessageSource
89       *
90       * @param source the MimeMessageSource
91       * @throws MessagingException 
92       */
93      public MimeMessageWrapper(Session session, MimeMessageSource source) throws MessagingException {
94          this(session);
95          this.source = source;
96      }
97  
98      /***
99       * A constructor that instantiates a MimeMessageWrapper based on
100      * a MimeMessageSource
101      *
102      * @param source the MimeMessageSource
103      * @throws MessagingException 
104      * @throws MessagingException 
105      */
106     public MimeMessageWrapper(MimeMessageSource source) throws MessagingException {
107         this(Session.getDefaultInstance(System.getProperties()),source);
108     }
109 
110     public MimeMessageWrapper(MimeMessage original) throws MessagingException {
111         this(Session.getDefaultInstance(System.getProperties()));
112         flags = original.getFlags();
113         
114         // if the original is an unmodified MimeMessageWrapped we clone the headers and
115         // take its source.
116         /* Temporary commented out because of JAMES-474
117         if (original instanceof MimeMessageWrapper && !((MimeMessageWrapper) original).bodyModified) {
118             source = ((MimeMessageWrapper) original).source;
119             // this probably speed up things
120             if (((MimeMessageWrapper) original).headers != null) {
121                 ByteArrayOutputStream temp = new ByteArrayOutputStream();
122                 InternetHeaders ih = ((MimeMessageWrapper) original).headers;
123                 MimeMessageUtil.writeHeadersTo(ih.getAllHeaderLines(),temp);
124                 headers = createInternetHeaders(new ByteArrayInputStream(temp.toByteArray()));
125                 headersModified = ((MimeMessageWrapper) original).headersModified;
126             }
127         }
128         */
129         
130         if (source == null) {
131             ByteArrayOutputStream bos;
132             int size = original.getSize();
133             if (size > 0)
134                 bos = new ByteArrayOutputStream(size);
135             else
136                 bos = new ByteArrayOutputStream();
137             try {
138                 original.writeTo(bos);
139                 bos.close();
140                 SharedByteArrayInputStream bis =
141                         new SharedByteArrayInputStream(bos.toByteArray());
142                 parse(bis);
143                 bis.close();
144                 saved = true;
145             } catch (IOException ex) {
146                 // should never happen, but just in case...
147                 throw new MessagingException("IOException while copying message",
148                                 ex);
149             }
150         }
151     }
152     
153     /***
154      * Returns the source ID of the MimeMessageSource that is supplying this
155      * with data.
156      * @see MimeMessageSource
157      */
158     public synchronized String getSourceId() {
159         return source != null ? source.getSourceId() : null;
160     }
161 
162     /***
163      * Load the message headers from the internal source.
164      *
165      * @throws MessagingException if an error is encountered while
166      *                            loading the headers
167      */
168     protected synchronized void loadHeaders() throws MessagingException {
169         if (headers != null) {
170             //Another thread has already loaded these headers
171             return;
172         } else if (source != null) { 
173             try {
174                 InputStream in = source.getInputStream();
175                 try {
176                     headers = createInternetHeaders(in);
177                 } finally {
178                     IOUtil.shutdownStream(in);
179                 }
180             } catch (IOException ioe) {
181                 throw new MessagingException("Unable to parse headers from stream: " + ioe.getMessage(), ioe);
182             }
183         } else {
184             throw new MessagingException("loadHeaders called for a message with no source, contentStream or stream");
185         }
186     }
187 
188     /***
189      * Load the complete MimeMessage from the internal source.
190      *
191      * @throws MessagingException if an error is encountered while
192      *                            loading the message
193      */
194     protected synchronized void loadMessage() throws MessagingException {
195         if (messageParsed) {
196             //Another thread has already loaded this message
197             return;
198         } else if (source != null) {
199             sourceIn = null;
200             try {
201                 sourceIn = source.getInputStream();
202     
203                 parse(sourceIn);
204                 // TODO is it ok?
205                 saved = true;
206                 
207             } catch (IOException ioe) {
208                 IOUtil.shutdownStream(sourceIn);
209                 sourceIn = null;
210                 throw new MessagingException("Unable to parse stream: " + ioe.getMessage(), ioe);
211             }
212         } else {
213             throw new MessagingException("loadHeaders called for an unparsed message with no source");
214         }
215     }
216 
217     /***
218      * Get whether the message has been modified.
219      *
220      * @return whether the message has been modified
221      */
222     public synchronized boolean isModified() {
223         return headersModified || bodyModified || modified;
224     }
225 
226     /***
227      * Rewritten for optimization purposes
228      */
229     public synchronized void writeTo(OutputStream os) throws IOException, MessagingException {
230         if (source != null && !isModified()) {
231             // We do not want to instantiate the message... just read from source
232             // and write to this outputstream
233             InputStream in = source.getInputStream();
234             try {
235                 MimeMessageUtil.copyStream(in, os);
236             } finally {
237                 IOUtil.shutdownStream(in);
238             }
239         } else {
240             writeTo(os, os);
241         }
242     }
243 
244     /***
245      * Rewritten for optimization purposes
246      */
247     public void writeTo(OutputStream os, String[] ignoreList) throws IOException, MessagingException {
248         writeTo(os, os, ignoreList);
249     }
250 
251     /***
252      * Write
253      */
254     public void writeTo(OutputStream headerOs, OutputStream bodyOs) throws IOException, MessagingException {
255         writeTo(headerOs, bodyOs, new String[0]);
256     }
257 
258     public synchronized void writeTo(OutputStream headerOs, OutputStream bodyOs, String[] ignoreList) throws IOException, MessagingException {
259         if (source != null && !isModified()) {
260             //We do not want to instantiate the message... just read from source
261             //  and write to this outputstream
262 
263             //First handle the headers
264             InputStream in = source.getInputStream();
265             try {
266                 InternetHeaders headers = new InternetHeaders(in);
267                 PrintWriter pos = new InternetPrintWriter(new BufferedWriter(new OutputStreamWriter(headerOs), 512), true);
268                 for (Enumeration e = headers.getNonMatchingHeaderLines(ignoreList); e.hasMoreElements(); ) {
269                     String header = (String)e.nextElement();
270                     pos.println(header);
271                 }
272                 pos.println();
273                 pos.flush();
274                 MimeMessageUtil.copyStream(in, bodyOs);
275             } finally {
276                 IOUtil.shutdownStream(in);
277             }
278         } else {
279             MimeMessageUtil.writeToInternal(this, headerOs, bodyOs, ignoreList);
280         }
281     }
282 
283     /***
284      * This is the MimeMessage implementation - this should return ONLY the
285      * body, not the entire message (should not count headers).  Will have
286      * to parse the message.
287      */
288     public int getSize() throws MessagingException {
289         if (!messageParsed) {
290             loadMessage();
291         }
292         return super.getSize();
293     }
294 
295     /***
296      * Corrects JavaMail 1.1 version which always returns -1.
297      * Only corrected for content less than 5000 bytes,
298      * to avoid memory hogging.
299      */
300     public int getLineCount() throws MessagingException {
301             InputStream in=null;
302         try{
303             in = getContentStream();
304         }catch(Exception e){
305             return -1;
306         }
307         if (in == null) {
308             return -1;
309         }
310         //Wrap input stream in LineNumberReader
311         //Not sure what encoding to use really...
312         try {
313             LineNumberReader counter;
314             if (getEncoding() != null) {
315                 counter = new LineNumberReader(new InputStreamReader(in, getEncoding()));
316             } else {
317                 counter = new LineNumberReader(new InputStreamReader(in));
318             }
319             //Read through all the data
320             char[] block = new char[4096];
321             while (counter.read(block) > -1) {
322                 //Just keep reading
323             }
324             return counter.getLineNumber();
325         } catch (IOException ioe) {
326             return -1;
327         } finally {
328             IOUtil.shutdownStream(in);
329         }
330     }
331 
332     /***
333      * Returns size of message, ie headers and content
334      */
335     public long getMessageSize() throws MessagingException {
336         if (source != null && !isModified()) {
337             try {
338                 return source.getMessageSize();
339             } catch (IOException ioe) {
340                 throw new MessagingException("Error retrieving message size", ioe);
341             }
342         } else {
343             return MimeMessageUtil.calculateMessageSize(this);
344         }
345     }
346     
347     /***
348      * We override all the "headers" access methods to be sure that we
349      * loaded the headers 
350      */
351     
352     public String[] getHeader(String name) throws MessagingException {
353         if (headers == null) {
354             loadHeaders();
355         }
356         return headers.getHeader(name);
357     }
358 
359     public String getHeader(String name, String delimiter) throws MessagingException {
360         if (headers == null) {
361             loadHeaders();
362         }
363         return headers.getHeader(name, delimiter);
364     }
365 
366     public Enumeration getAllHeaders() throws MessagingException {
367         if (headers == null) {
368             loadHeaders();
369         }
370         return headers.getAllHeaders();
371     }
372 
373     public Enumeration getMatchingHeaders(String[] names) throws MessagingException {
374         if (headers == null) {
375             loadHeaders();
376         }
377         return headers.getMatchingHeaders(names);
378     }
379 
380     public Enumeration getNonMatchingHeaders(String[] names) throws MessagingException {
381         if (headers == null) {
382             loadHeaders();
383         }
384         return headers.getNonMatchingHeaders(names);
385     }
386 
387     public Enumeration getAllHeaderLines() throws MessagingException {
388         if (headers == null) {
389             loadHeaders();
390         }
391         return headers.getAllHeaderLines();
392     }
393 
394     public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
395         if (headers == null) {
396             loadHeaders();
397         }
398         return headers.getMatchingHeaderLines(names);
399     }
400 
401     public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
402         if (headers == null) {
403             loadHeaders();
404         }
405         return headers.getNonMatchingHeaderLines(names);
406     }
407 
408 
409     private synchronized void checkModifyHeaders() throws MessagingException {
410         // Disable only-header loading optimizations for JAMES-559
411         if (!messageParsed) {
412             loadMessage();
413         }
414         // End JAMES-559
415         if (headers == null) {
416             loadHeaders();
417         }
418         modified = true;
419         saved = false;
420         headersModified = true;
421     }
422 
423     public void setHeader(String name, String value) throws MessagingException {
424         checkModifyHeaders();
425         super.setHeader(name, value);
426     }
427 
428     public void addHeader(String name, String value) throws MessagingException {
429         checkModifyHeaders();
430         super.addHeader(name, value);
431     }
432 
433     public void removeHeader(String name) throws MessagingException {
434         checkModifyHeaders();
435         super.removeHeader(name);
436     }
437 
438     public void addHeaderLine(String line) throws MessagingException {
439         checkModifyHeaders();
440         super.addHeaderLine(line);
441     }
442 
443 
444     /***
445      * The message is changed when working with headers and when altering the content.
446      * Every method that alter the content will fallback to this one.
447      * 
448      * @see javax.mail.Part#setDataHandler(javax.activation.DataHandler)
449      */
450     public synchronized void setDataHandler(DataHandler arg0) throws MessagingException {
451         modified = true;
452         saved = false;
453         bodyModified = true;
454         super.setDataHandler(arg0);
455     }
456 
457     /***
458      * @see org.apache.avalon.framework.activity.Disposable#dispose()
459      */
460     public void dispose() {
461         if (sourceIn != null) {
462             IOUtil.shutdownStream(sourceIn);
463         }
464         if (source != null) {
465             ContainerUtil.dispose(source);
466         }
467     }
468 
469     /***
470      * @see javax.mail.internet.MimeMessage#parse(java.io.InputStream)
471      */
472     protected synchronized void parse(InputStream is) throws MessagingException {
473         // the super implementation calls
474         // headers = createInternetHeaders(is);
475         super.parse(is);
476         messageParsed = true;
477     }
478 
479     /***
480      * If we already parsed the headers then we simply return the updated ones.
481      * Otherwise we parse
482      * 
483      * @see javax.mail.internet.MimeMessage#createInternetHeaders(java.io.InputStream)
484      */
485     protected synchronized InternetHeaders createInternetHeaders(InputStream is) throws MessagingException {
486         /* This code is no more needed: see JAMES-570 and new tests
487            
488          * InternetHeaders can be a bit awkward to work with due to
489          * its own internal handling of header order.  This hack may
490          * not always be necessary, but for now we are trying to
491          * ensure that there is a Return-Path header, even if just a
492          * placeholder, so that later, e.g., in LocalDelivery, when we
493          * call setHeader, it will remove any other Return-Path
494          * headers, and ensure that ours is on the top. addHeader
495          * handles header order, but not setHeader. This may change in
496          * future JavaMail.  But if there are other Return-Path header
497          * values, let's drop our placeholder.
498 
499         MailHeaders newHeaders = new MailHeaders(new ByteArrayInputStream((RFC2822Headers.RETURN_PATH + ": placeholder").getBytes()));
500         newHeaders.setHeader(RFC2822Headers.RETURN_PATH, null);
501         newHeaders.load(is);
502         String[] returnPathHeaders = newHeaders.getHeader(RFC2822Headers.RETURN_PATH);
503         if (returnPathHeaders.length > 1) newHeaders.setHeader(RFC2822Headers.RETURN_PATH, returnPathHeaders[1]);
504         */
505         
506         // Keep this: skip the headers from the stream
507         // we could put that code in the else and simple write an "header" skipping
508         // reader for the others.
509         MailHeaders newHeaders = new MailHeaders(is);
510         
511         if (headers != null) {
512             return headers;
513         } else {
514             return newHeaders;
515         }
516     }
517 
518     /***
519      * @see javax.mail.internet.MimeMessage#getContentStream()
520      */
521     protected InputStream getContentStream() throws MessagingException {
522         if (!messageParsed) {
523             loadMessage();
524         }
525         return super.getContentStream();
526     }
527 
528     /***
529      * @see javax.mail.internet.MimeMessage#getRawInputStream()
530      */
531     public InputStream getRawInputStream() throws MessagingException {
532         if (!messageParsed && !isModified() && source != null) {
533             InputStream is;
534             try {
535                 is = source.getInputStream();
536                 // skip the headers.
537                 new MailHeaders(is);
538                 return is;
539             } catch (IOException e) {
540                 throw new MessagingException("Unable to read the stream: " + e.getMessage(), e);
541             }
542         } else return super.getRawInputStream();
543     }
544 
545     
546     
547 }