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
117
118
119
120
121
122
123
124
125
126
127
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
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
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
199 return;
200 } else if (source != null) {
201 sourceIn = null;
202 try {
203 sourceIn = source.getInputStream();
204
205 parse(sourceIn);
206
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
234
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
263
264
265
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
313
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
322 char[] block = new char[4096];
323 while (counter.read(block) > -1) {
324
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
413 if (!messageParsed) {
414 loadMessage();
415 }
416
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
476
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
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
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
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 }