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.mailet.Mail;
27 import org.apache.mailet.MailAddress;
28 import org.apache.mailet.base.RFC2822Headers;
29
30 import javax.mail.MessagingException;
31 import javax.mail.internet.InternetAddress;
32 import javax.mail.internet.MimeMessage;
33 import javax.mail.internet.ParseException;
34
35 import java.io.ByteArrayInputStream;
36 import java.io.ByteArrayOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.ObjectInputStream;
40 import java.io.ObjectOutputStream;
41 import java.io.OptionalDataException;
42 import java.io.OutputStream;
43 import java.io.Serializable;
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.Date;
47 import java.util.HashMap;
48 import java.util.Iterator;
49
50 /**
51 * <P>Wraps a MimeMessage adding routing information (from SMTP) and some simple
52 * API enhancements.</P>
53 * <P>From James version > 2.2.0a8 "mail attributes" have been added.
54 * Backward and forward compatibility is supported:
55 * messages stored in file repositories <I>without</I> attributes by James version <= 2.2.0a8
56 * will be processed by later versions as having an empty attributes hashmap;
57 * messages stored in file repositories <I>with</I> attributes by James version > 2.2.0a8
58 * will be processed by previous versions, ignoring the attributes.</P>
59 *
60 * @version CVS $Revision: 717869 $ $Date: 2008-11-15 15:56:18 +0000 (Sat, 15 Nov 2008) $
61 */
62 public class MailImpl implements Disposable, Mail {
63
64 /**
65 * We hardcode the serialVersionUID so that from James 1.2 on,
66 * MailImpl will be deserializable (so your mail doesn't get lost)
67 */
68 public static final long serialVersionUID = -4289663364703986260L;
69 /**
70 * The error message, if any, associated with this mail.
71 */
72 private String errorMessage;
73 /**
74 * The state of this mail, which determines how it is processed.
75 */
76 private String state;
77 /**
78 * The MimeMessage that holds the mail data.
79 */
80 private MimeMessage message;
81 /**
82 * The sender of this mail.
83 */
84 private MailAddress sender;
85 /**
86 * The collection of recipients to whom this mail was sent.
87 */
88 private Collection recipients;
89 /**
90 * The identifier for this mail message
91 */
92 private String name;
93 /**
94 * The remote host from which this mail was sent.
95 */
96 private String remoteHost = "localhost";
97 /**
98 * The remote address from which this mail was sent.
99 */
100 private String remoteAddr = "127.0.0.1";
101 /**
102 * The last time this message was updated.
103 */
104 private Date lastUpdated = new Date();
105 /**
106 * Attributes added to this MailImpl instance
107 */
108 private HashMap attributes;
109 /**
110 * A constructor that creates a new, uninitialized MailImpl
111 */
112 public MailImpl() {
113 setState(Mail.DEFAULT);
114 attributes = new HashMap();
115 }
116 /**
117 * A constructor that creates a MailImpl with the specified name,
118 * sender, and recipients.
119 *
120 * @param name the name of the MailImpl
121 * @param sender the sender for this MailImpl
122 * @param recipients the collection of recipients of this MailImpl
123 */
124 public MailImpl(String name, MailAddress sender, Collection recipients) {
125 this();
126 this.name = name;
127 this.sender = sender;
128 this.recipients = null;
129
130 // Copy the recipient list
131 if (recipients != null) {
132 Iterator theIterator = recipients.iterator();
133 this.recipients = new ArrayList();
134 while (theIterator.hasNext()) {
135 this.recipients.add(theIterator.next());
136 }
137 }
138 }
139
140 /**
141 * Create a copy of the input mail and assign it a new name
142 *
143 * @param mail original mail
144 * @throws MessagingException when the message is not clonable
145 */
146 public MailImpl(Mail mail) throws MessagingException {
147 this(mail, newName(mail));
148 }
149
150 /**
151 * @param mail
152 * @param newName
153 * @throws MessagingException
154 */
155 public MailImpl(Mail mail, String newName) throws MessagingException {
156 this(newName, mail.getSender(), mail.getRecipients(), mail.getMessage());
157 setRemoteHost(mail.getRemoteHost());
158 setRemoteAddr(mail.getRemoteAddr());
159 setLastUpdated(mail.getLastUpdated());
160 try {
161 if (mail instanceof MailImpl) {
162 setAttributesRaw((HashMap) cloneSerializableObject(((MailImpl) mail).getAttributesRaw()));
163 } else {
164 HashMap attribs = new HashMap();
165 for (Iterator i = mail.getAttributeNames(); i.hasNext(); ) {
166 String hashKey = (String) i.next();
167 attribs.put(hashKey,cloneSerializableObject(mail.getAttribute(hashKey)));
168 }
169 setAttributesRaw(attribs);
170 }
171 } catch (IOException e) {
172 // should never happen for in memory streams
173 setAttributesRaw(new HashMap());
174 } catch (ClassNotFoundException e) {
175 // should never happen as we just serialized it
176 setAttributesRaw(new HashMap());
177 }
178 }
179
180 /**
181 * A constructor that creates a MailImpl with the specified name,
182 * sender, recipients, and message data.
183 *
184 * @param name the name of the MailImpl
185 * @param sender the sender for this MailImpl
186 * @param recipients the collection of recipients of this MailImpl
187 * @param messageIn a stream containing the message source
188 */
189 public MailImpl(String name, MailAddress sender, Collection recipients, InputStream messageIn)
190 throws MessagingException {
191 this(name, sender, recipients);
192 MimeMessageSource source = new MimeMessageInputStreamSource(name, messageIn);
193 // if MimeMessageCopyOnWriteProxy throws an error in the constructor we
194 // have to manually care disposing our source.
195 try {
196 this.setMessage(new MimeMessageCopyOnWriteProxy(source));
197 } catch (MessagingException e) {
198 ContainerUtil.dispose(source);
199 throw e;
200 }
201 }
202
203 /**
204 * A constructor that creates a MailImpl with the specified name,
205 * sender, recipients, and MimeMessage.
206 *
207 * @param name the name of the MailImpl
208 * @param sender the sender for this MailImpl
209 * @param recipients the collection of recipients of this MailImpl
210 * @param message the MimeMessage associated with this MailImpl
211 */
212 public MailImpl(String name, MailAddress sender, Collection recipients, MimeMessage message) throws MessagingException {
213 this(name, sender, recipients);
214 this.setMessage(new MimeMessageCopyOnWriteProxy(message));
215 }
216
217 /**
218 * Gets the MailAddress corresponding to the existing "Return-Path" of
219 * <I>message</I>.
220 * If missing or empty returns <CODE>null</CODE>,
221 */
222 private MailAddress getReturnPath(MimeMessage message) throws MessagingException {
223 MailAddress mailAddress = null;
224 String[] returnPathHeaders = message.getHeader(RFC2822Headers.RETURN_PATH);
225 String returnPathHeader = null;
226 if (returnPathHeaders != null) {
227 returnPathHeader = returnPathHeaders[0];
228 if (returnPathHeader != null) {
229 returnPathHeader = returnPathHeader.trim();
230 if (!returnPathHeader.equals("<>")) {
231 try {
232 mailAddress = new MailAddress(new InternetAddress(returnPathHeader, false));
233 } catch (ParseException pe) {
234 throw new MessagingException("Could not parse address: " + returnPathHeader + " from " + message.getHeader(RFC2822Headers.RETURN_PATH, ", "), pe);
235 }
236 }
237 }
238 }
239 return mailAddress;
240 }
241 /**
242 * Duplicate the MailImpl.
243 *
244 * @return a MailImpl that is a duplicate of this one
245 */
246 public Mail duplicate() {
247 return duplicate(name);
248 }
249 /**
250 * Duplicate the MailImpl, replacing the mail name with the one
251 * passed in as an argument.
252 *
253 * @param newName the name for the duplicated mail
254 *
255 * @return a MailImpl that is a duplicate of this one with a different name
256 */
257 public Mail duplicate(String newName) {
258 try {
259 return new MailImpl(this, newName);
260 } catch (MessagingException me) {
261 // Ignored. Return null in the case of an error.
262 }
263 return null;
264 }
265 /**
266 * Get the error message associated with this MailImpl.
267 *
268 * @return the error message associated with this MailImpl
269 */
270 public String getErrorMessage() {
271 return errorMessage;
272 }
273 /**
274 * Get the MimeMessage associated with this MailImpl.
275 *
276 * @return the MimeMessage associated with this MailImpl
277 */
278 public MimeMessage getMessage() throws MessagingException {
279 return message;
280 }
281
282 /**
283 * Set the name of this MailImpl.
284 *
285 * @param name the name of this MailImpl
286 */
287 public void setName(String name) {
288 this.name = name;
289 }
290 /**
291 * Get the name of this MailImpl.
292 *
293 * @return the name of this MailImpl
294 */
295 public String getName() {
296 return name;
297 }
298 /**
299 * Get the recipients of this MailImpl.
300 *
301 * @return the recipients of this MailImpl
302 */
303 public Collection getRecipients() {
304 return recipients;
305 }
306 /**
307 * Get the sender of this MailImpl.
308 *
309 * @return the sender of this MailImpl
310 */
311 public MailAddress getSender() {
312 return sender;
313 }
314 /**
315 * Get the state of this MailImpl.
316 *
317 * @return the state of this MailImpl
318 */
319 public String getState() {
320 return state;
321 }
322 /**
323 * Get the remote host associated with this MailImpl.
324 *
325 * @return the remote host associated with this MailImpl
326 */
327 public String getRemoteHost() {
328 return remoteHost;
329 }
330 /**
331 * Get the remote address associated with this MailImpl.
332 *
333 * @return the remote address associated with this MailImpl
334 */
335 public String getRemoteAddr() {
336 return remoteAddr;
337 }
338 /**
339 * Get the last updated time for this MailImpl.
340 *
341 * @return the last updated time for this MailImpl
342 */
343 public Date getLastUpdated() {
344 return lastUpdated;
345 }
346
347 /**
348 * <p>Return the size of the message including its headers.
349 * MimeMessage.getSize() method only returns the size of the
350 * message body.</p>
351 *
352 * <p>Note: this size is not guaranteed to be accurate - see Sun's
353 * documentation of MimeMessage.getSize().</p>
354 *
355 * @return approximate size of full message including headers.
356 *
357 * @throws MessagingException if a problem occurs while computing the message size
358 */
359 public long getMessageSize() throws MessagingException {
360 return MimeMessageUtil.getMessageSize(message);
361 }
362
363 /**
364 * Set the error message associated with this MailImpl.
365 *
366 * @param msg the new error message associated with this MailImpl
367 */
368 public void setErrorMessage(String msg) {
369 this.errorMessage = msg;
370 }
371 /**
372 * Set the MimeMessage associated with this MailImpl.
373 *
374 * @param message the new MimeMessage associated with this MailImpl
375 */
376 public void setMessage(MimeMessage message) {
377
378 //TODO: We should use the MimeMessageCopyOnWriteProxy
379 // everytime we set the MimeMessage. We should
380 // investigate if we should wrap it here
381
382 if (this.message != message) {
383 // If a setMessage is called on a Mail that already have a message
384 // (discouraged) we have to make sure that the message we remove is
385 // correctly unreferenced and disposed, otherwise it will keep locks
386 if (this.message != null) {
387 ContainerUtil.dispose(this.message);
388 }
389 this.message = message;
390 }
391 }
392 /**
393 * Set the recipients for this MailImpl.
394 *
395 * @param recipients the recipients for this MailImpl
396 */
397 public void setRecipients(Collection recipients) {
398 this.recipients = recipients;
399 }
400 /**
401 * Set the sender of this MailImpl.
402 *
403 * @param sender the sender of this MailImpl
404 */
405 public void setSender(MailAddress sender) {
406 this.sender = sender;
407 }
408 /**
409 * Set the state of this MailImpl.
410 *
411 * @param state the state of this MailImpl
412 */
413 public void setState(String state) {
414 this.state = state;
415 }
416 /**
417 * Set the remote address associated with this MailImpl.
418 *
419 * @param remoteHost the new remote host associated with this MailImpl
420 */
421 public void setRemoteHost(String remoteHost) {
422 this.remoteHost = remoteHost;
423 }
424 /**
425 * Set the remote address associated with this MailImpl.
426 *
427 * @param remoteAddr the new remote address associated with this MailImpl
428 */
429 public void setRemoteAddr(String remoteAddr) {
430 this.remoteAddr = remoteAddr;
431 }
432 /**
433 * Set the date this mail was last updated.
434 *
435 * @param lastUpdated the date the mail was last updated
436 */
437 public void setLastUpdated(Date lastUpdated) {
438 // Make a defensive copy to ensure that the date
439 // doesn't get changed external to the class
440 if (lastUpdated != null) {
441 lastUpdated = new Date(lastUpdated.getTime());
442 }
443 this.lastUpdated = lastUpdated;
444 }
445 /**
446 * Writes the message out to an OutputStream.
447 *
448 * @param out the OutputStream to which to write the content
449 *
450 * @throws MessagingException if the MimeMessage is not set for this MailImpl
451 * @throws IOException if an error occurs while reading or writing from the stream
452 */
453 public void writeMessageTo(OutputStream out) throws IOException, MessagingException {
454 if (message != null) {
455 message.writeTo(out);
456 } else {
457 throw new MessagingException("No message set for this MailImpl.");
458 }
459 }
460
461 // Serializable Methods
462 // TODO: These need some work. Currently very tightly coupled to
463 // the internal representation.
464 /**
465 * Read the MailImpl from an <code>ObjectInputStream</code>.
466 *
467 * @param in the ObjectInputStream from which the object is read
468 *
469 * @throws IOException if an error occurs while reading from the stream
470 * @throws ClassNotFoundException ?
471 * @throws ClassCastException if the serialized objects are not of the appropriate type
472 */
473 private void readObject(java.io.ObjectInputStream in)
474 throws IOException, ClassNotFoundException {
475 try {
476 Object obj = in.readObject();
477 if (obj == null) {
478 sender = null;
479 } else if (obj instanceof String) {
480 sender = new MailAddress((String) obj);
481 } else if (obj instanceof MailAddress) {
482 sender = (MailAddress) obj;
483 }
484 } catch (ParseException pe) {
485 throw new IOException("Error parsing sender address: " + pe.getMessage());
486 }
487 recipients = (Collection) in.readObject();
488 state = (String) in.readObject();
489 errorMessage = (String) in.readObject();
490 name = (String) in.readObject();
491 remoteHost = (String) in.readObject();
492 remoteAddr = (String) in.readObject();
493 setLastUpdated((Date) in.readObject());
494 // the following is under try/catch to be backwards compatible
495 // with messages created with James version <= 2.2.0a8
496 try {
497 attributes = (HashMap) in.readObject();
498 } catch (OptionalDataException ode) {
499 if (ode.eof) {
500 attributes = new HashMap();
501 } else {
502 throw ode;
503 }
504 }
505 }
506 /**
507 * Write the MailImpl to an <code>ObjectOutputStream</code>.
508 *
509 * @param out the ObjectOutputStream to which the object is written
510 *
511 * @throws IOException if an error occurs while writing to the stream
512 */
513 private void writeObject(java.io.ObjectOutputStream out) throws IOException {
514 out.writeObject(sender);
515 out.writeObject(recipients);
516 out.writeObject(state);
517 out.writeObject(errorMessage);
518 out.writeObject(name);
519 out.writeObject(remoteHost);
520 out.writeObject(remoteAddr);
521 out.writeObject(lastUpdated);
522 out.writeObject(attributes);
523 }
524
525 /**
526 * @see org.apache.avalon.framework.activity.Disposable#dispose()
527 */
528 public void dispose() {
529 ContainerUtil.dispose(message);
530 message = null;
531 }
532
533 /**
534 * This method is necessary, when Mail repositories needs to deal
535 * explicitly with storing Mail attributes as a Serializable
536 * Note: This method is not exposed in the Mail interface,
537 * it is for internal use by James only.
538 * @return Serializable of the entire attributes collection
539 * @since 2.2.0
540 **/
541 public HashMap getAttributesRaw () {
542 return attributes;
543 }
544
545 /**
546 * This method is necessary, when Mail repositories needs to deal
547 * explicitly with retriving Mail attributes as a Serializable
548 * Note: This method is not exposed in the Mail interface,
549 * it is for internal use by James only.
550 * @param attr Serializable of the entire attributes collection
551 * @since 2.2.0
552 **/
553 public void setAttributesRaw (HashMap attr)
554 {
555 this.attributes = (attr == null) ? new HashMap() : attr;
556 }
557
558 /**
559 * @see org.apache.mailet.Mail#getAttribute(String)
560 * @since 2.2.0
561 */
562 public Serializable getAttribute(String key) {
563 return (Serializable)attributes.get(key);
564 }
565 /**
566 * @see org.apache.mailet.Mail#setAttribute(String,Serializable)
567 * @since 2.2.0
568 */
569 public Serializable setAttribute(String key, Serializable object) {
570 return (Serializable)attributes.put(key, object);
571 }
572 /**
573 * @see org.apache.mailet.Mail#removeAttribute(String)
574 * @since 2.2.0
575 */
576 public Serializable removeAttribute(String key) {
577 return (Serializable)attributes.remove(key);
578 }
579 /**
580 * @see org.apache.mailet.Mail#removeAllAttributes()
581 * @since 2.2.0
582 */
583 public void removeAllAttributes() {
584 attributes.clear();
585 }
586 /**
587 * @see org.apache.mailet.Mail#getAttributeNames()
588 * @since 2.2.0
589 */
590 public Iterator getAttributeNames() {
591 return attributes.keySet().iterator();
592 }
593 /**
594 * @see org.apache.mailet.Mail#hasAttributes()
595 * @since 2.2.0
596 */
597 public boolean hasAttributes() {
598 return !attributes.isEmpty();
599 }
600
601
602 /**
603 * This methods provide cloning for serializable objects.
604 * Mail Attributes are Serializable but not Clonable so we need a deep copy
605 *
606 * @param o Object to be cloned
607 * @return the cloned Object
608 * @throws IOException
609 * @throws ClassNotFoundException
610 */
611 private static Object cloneSerializableObject(Object o) throws IOException, ClassNotFoundException {
612 ByteArrayOutputStream b = new ByteArrayOutputStream();
613 ObjectOutputStream out = new ObjectOutputStream(b);
614 out.writeObject(o);
615 out.flush();
616 out.close();
617 ByteArrayInputStream bi=new ByteArrayInputStream(b.toByteArray());
618 ObjectInputStream in = new ObjectInputStream(bi);
619 Object no = in.readObject();
620 return no;
621 }
622
623
624 private static final java.util.Random random = new java.util.Random(); // Used to generate new mail names
625
626 /**
627 * Create a unique new primary key name for the given MailObject.
628 *
629 * @param mail the mail to use as the basis for the new mail name
630 * @return a new name
631 */
632 public static String newName(Mail mail) throws MessagingException {
633 String oldName = mail.getName();
634
635 // Checking if the original mail name is too long, perhaps because of a
636 // loop caused by a configuration error.
637 // it could cause a "null pointer exception" in AvalonMailRepository much
638 // harder to understand.
639 if (oldName.length() > 76) {
640 int count = 0;
641 int index = 0;
642 while ((index = oldName.indexOf('!', index + 1)) >= 0) {
643 count++;
644 }
645 // It looks like a configuration loop. It's better to stop.
646 if (count > 7) {
647 throw new MessagingException("Unable to create a new message name: too long."
648 + " Possible loop in config.xml.");
649 }
650 else {
651 oldName = oldName.substring(0, 76);
652 }
653 }
654
655 StringBuffer nameBuffer =
656 new StringBuffer(64)
657 .append(oldName)
658 .append("-!")
659 .append(random.nextInt(1048576));
660 return nameBuffer.toString();
661 }
662
663 }