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.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 }