View Javadoc

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