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