View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.james.jcr;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.ObjectInputStream;
24  import java.io.ObjectOutputStream;
25  import java.io.PipedInputStream;
26  import java.io.PipedOutputStream;
27  import java.io.Serializable;
28  import java.util.ArrayList;
29  import java.util.Calendar;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.Date;
33  import java.util.Iterator;
34  import java.util.Properties;
35  
36  import javax.jcr.Credentials;
37  import javax.jcr.Node;
38  import javax.jcr.NodeIterator;
39  import javax.jcr.PathNotFoundException;
40  import javax.jcr.Property;
41  import javax.jcr.PropertyIterator;
42  import javax.jcr.PropertyType;
43  import javax.jcr.Repository;
44  import javax.jcr.RepositoryException;
45  import javax.jcr.Session;
46  import javax.jcr.Value;
47  import javax.jcr.query.Query;
48  import javax.jcr.query.QueryManager;
49  import javax.mail.MessagingException;
50  import javax.mail.internet.MimeMessage;
51  
52  import org.apache.commons.logging.Log;
53  import org.apache.commons.logging.LogFactory;
54  import org.apache.jackrabbit.util.ISO9075;
55  import org.apache.jackrabbit.util.Text;
56  import org.apache.james.core.MailImpl;
57  import org.apache.james.services.MailRepository;
58  import org.apache.mailet.Mail;
59  import org.apache.mailet.MailAddress;
60  
61  /**
62   * Mail repository that is backed by a JCR content repository.
63   */
64  public class JCRMailRepository extends AbstractJCRRepository implements MailRepository {
65  
66      /**
67       * Logger instance.
68       */
69      private static final Log LOGGER = LogFactory.getLog(JCRMailRepository.class.getName());
70  
71      /**
72       * For setter injection.
73       */
74      public JCRMailRepository() {
75          super(LOGGER);
76          this.path = "james/mail";
77      }
78      
79      /**
80       * Maximal constructor for injection.
81       * @param repository not null
82       * @param credentials login credentials for accessing the repository
83       * or null to use default credentials
84       * @param workspace name of the workspace used as the mail repository.
85       * or null to use default workspace
86       * @param path path (relative to root) of the user node within the workspace,
87       * or null to use default.
88       */
89      public JCRMailRepository(Repository repository, Credentials credentials, String workspace, String path, Log logger) {
90          super(repository, credentials, workspace, path, logger);
91      }
92  
93      /**
94       * Minimal constructor for injection.
95       * @param repository not null
96       */
97      public JCRMailRepository(Repository repository, Log logger) {
98          super(repository, logger);
99      }
100 
101 
102 
103     public Iterator list() throws MessagingException {
104         try {
105             Session session = login();
106             try {
107                 Collection keys = new ArrayList();
108                 QueryManager manager = session.getWorkspace().getQueryManager();
109                 Query query = manager.createQuery(
110                         "/jcr:root/" + path + "//element(*,james:mail)",
111                         Query.XPATH);
112                 NodeIterator iterator = query.execute().getNodes();
113                 while (iterator.hasNext()) {
114                     String name = iterator.nextNode().getName();
115                     keys.add(Text.unescapeIllegalJcrChars(name));
116                 }
117                 return keys.iterator();
118             } finally {
119                 session.logout();
120             }
121         } catch (RepositoryException e) {
122             throw new MessagingException("Unable to list messages", e);
123         }
124     }
125 
126     public Mail retrieve(String key) throws MessagingException {
127         try {
128             Session session = login();
129             try {
130                 String name = toSafeName(key);
131                 QueryManager manager = session.getWorkspace().getQueryManager();
132                 Query query = manager.createQuery(
133                         "/jcr:root/" + path + "//element(" + name + ",james:mail)",
134                         Query.XPATH);
135                 NodeIterator iterator = query.execute().getNodes();
136                 if (iterator.hasNext()) {
137                     return getMail(iterator.nextNode());
138                 } else {
139                     return null;
140                 }
141             } finally {
142                 session.logout();
143             }
144         } catch (IOException e) {
145             throw new MessagingException(
146                     "Unable to retrieve message: " + key, e);
147         } catch (RepositoryException e) {
148             throw new MessagingException(
149                     "Unable to retrieve message: " + key, e);
150         } 
151     }
152 
153     public void store(Mail mail) throws MessagingException {
154         try {
155             Session session = login();
156             try {
157                 String name = Text.escapeIllegalJcrChars(mail.getName());
158                 final String xpath = "/jcr:root/" + path + "//element(" + name + ",james:mail)";
159                 NodeIterator iterator = query(session, xpath);
160                 if (iterator.hasNext()) {
161                     while (iterator.hasNext()) {
162                         setMail(iterator.nextNode(), mail);
163                     }
164                 } else {
165                     Node parent = session.getRootNode().getNode(path);
166                     Node node = parent.addNode(name, "james:mail");
167                     Node resource = node.addNode("jcr:content", "nt:resource");
168                     resource.setProperty("jcr:mimeType", "message/rfc822");
169                     setMail(node, mail);
170                 }
171                 session.save();
172                 logger.info("Mail " + mail.getName() + " stored in repository");
173             } finally {
174                 session.logout();
175             }
176         } catch (IOException e) {
177             throw new MessagingException(
178                     "Unable to store message: " + mail.getName(), e);
179         } catch (RepositoryException e) {
180             throw new MessagingException(
181                     "Unable to store message: " + mail.getName(), e);
182         }
183     }
184 
185     public void remove(String key) throws MessagingException {
186         try {
187             Session session = login();
188             try {
189                 String name = ISO9075.encode(Text.escapeIllegalJcrChars(key));
190                 QueryManager manager = session.getWorkspace().getQueryManager();
191                 Query query = manager.createQuery(
192                         "/jcr:root/" + path + "//element(" + name + ",james:mail)",
193                         Query.XPATH);
194                 NodeIterator nodes = query.execute().getNodes();
195                 if (nodes.hasNext()) {
196                     while (nodes.hasNext()) {
197                         nodes.nextNode().remove();
198                     }
199                     session.save();
200                     logger.info("Mail " + key + " removed from repository");
201                 } else {
202                     logger.warn("Mail " + key + " not found");
203                 }
204             } finally {
205                 session.logout();
206             }
207         } catch (RepositoryException e) {
208             throw new MessagingException("Unable to remove message: " + key, e);
209         }
210     }
211 
212     public void remove(Mail mail) throws MessagingException {
213         remove(mail.getName());
214     }
215 
216     public void remove(Collection mails) throws MessagingException {
217         try {
218             Session session = login();
219             try {
220                 QueryManager manager = session.getWorkspace().getQueryManager();
221                 Iterator iterator = mails.iterator();
222                 while (iterator.hasNext()) {
223                     Mail mail = (Mail) iterator.next();
224                     try {
225                         String name = ISO9075.encode(
226                                 Text.escapeIllegalJcrChars(mail.getName()));
227                         Query query = manager.createQuery(
228                                 "/jcr:root/" + path + "//element(" + name + ",james:mail)",
229                                 Query.XPATH);
230                         NodeIterator nodes = query.execute().getNodes();
231                         while (nodes.hasNext()) {
232                             nodes.nextNode().remove();
233                         }
234                     } catch (PathNotFoundException e) {
235                         logger.warn("Mail " + mail.getName() + " not found");
236                     }
237                 }
238                 session.save();
239                 logger.info("Mail collection removed from repository");
240             } finally {
241                 session.logout();
242             }
243         } catch (RepositoryException e) {
244             throw new MessagingException("Unable to remove messages", e);
245         }
246     }
247 
248     public boolean lock(String key) throws MessagingException {
249         return false;
250     }
251 
252     public boolean unlock(String key) throws MessagingException {
253         return false;
254     }
255 
256     //-------------------------------------------------------------< private >
257 
258     /**
259      * Reads a mail message from the given mail node.
260      *
261      * @param node mail node
262      * @return mail message
263      * @throws MessagingException if a messaging error occurs
264      * @throws RepositoryException if a repository error occurs
265      * @throws IOException if an IO error occurs
266      */
267     private Mail getMail(Node node)
268             throws MessagingException, RepositoryException, IOException {
269         String name = Text.unescapeIllegalJcrChars(node.getName());
270         MailImpl mail = new MailImpl(
271                 name, getSender(node), getRecipients(node),
272                 getMessage(node));
273         mail.setState(getState(node));
274         mail.setLastUpdated(getLastUpdated(node));
275         mail.setErrorMessage(getError(node));
276         mail.setRemoteHost(getRemoteHost(node));
277         mail.setRemoteAddr(getRemoteAddr(node));
278         getAttributes(node, mail);
279         return mail;
280     }
281 
282     /**
283      * Writes the mail message to the given mail node.
284      *
285      * @param node mail node
286      * @param mail mail message
287      * @throws MessagingException if a messaging error occurs
288      * @throws RepositoryException if a repository error occurs
289      * @throws IOException if an IO error occurs
290      */
291     private void setMail(Node node, Mail mail)
292             throws MessagingException, RepositoryException, IOException {
293         setState(node, mail.getState());
294         setLastUpdated(node, mail.getLastUpdated());
295         setError(node, mail.getErrorMessage());
296         setRemoteHost(node, mail.getRemoteHost());
297         setRemoteAddr(node, mail.getRemoteAddr());
298         setSender(node, mail.getSender());
299         setRecipients(node, mail.getRecipients());
300         setMessage(node, mail.getMessage());
301         setAttributes(node, mail);
302     }
303 
304     /**
305      * Reads the message state from the james:state property.
306      *
307      * @param node mail node
308      * @return message state, or {@link Mail#DEFAULT} if not set
309      * @throws RepositoryException if a repository error occurs
310      */
311     private String getState(Node node) throws RepositoryException {
312         try {
313             return node.getProperty("james:state").getString();
314         } catch (PathNotFoundException e) {
315             return Mail.DEFAULT;
316         }
317     }
318 
319     /**
320      * Writes the message state to the james:state property.
321      *
322      * @param node mail node
323      * @param state message state
324      * @throws RepositoryException if a repository error occurs
325      */
326     private void setState(Node node, String state) throws RepositoryException {
327         node.setProperty("james:state", state);
328     }
329 
330     /**
331      * Reads the update timestamp from the jcr:content/jcr:lastModified property.
332      *
333      * @param node mail node
334      * @return update timestamp
335      * @throws RepositoryException if a repository error occurs
336      */
337     private Date getLastUpdated(Node node) throws RepositoryException {
338         try {
339             node = node.getNode("jcr:content");
340         } catch (PathNotFoundException e) {
341             node = node.getProperty("jcr:content").getNode();
342         }
343         return node.getProperty("jcr:lastModified").getDate().getTime();
344     }
345 
346     /**
347      * Writes the update timestamp to the jcr:content/jcr:lastModified property.
348      *
349      * @param node mail node
350      * @param updated update timestamp, or <code>null</code> if not set
351      * @throws RepositoryException if a repository error occurs
352      */
353     private void setLastUpdated(Node node, Date updated)
354             throws RepositoryException {
355         try {
356             node = node.getNode("jcr:content");
357         } catch (PathNotFoundException e) {
358             node = node.getProperty("jcr:content").getNode();
359         }
360         Calendar calendar = Calendar.getInstance();
361         if (updated != null) {
362             calendar.setTime(updated);
363         }
364         node.setProperty("jcr:lastModified", calendar);
365     }
366 
367     /**
368      * Reads the error message from the james:error property.
369      *
370      * @param node mail node
371      * @return error message, or <code>null</code> if not set
372      * @throws RepositoryException if a repository error occurs
373      */
374     private String getError(Node node) throws RepositoryException {
375         try {
376             return node.getProperty("james:error").getString();
377         } catch (PathNotFoundException e) {
378             return null;
379         }
380     }
381 
382     /**
383      * Writes the error message to the james:error property.
384      *
385      * @param node mail node
386      * @param error error message
387      * @throws RepositoryException if a repository error occurs
388      */
389     private void setError(Node node, String error) throws RepositoryException {
390         node.setProperty("james:error", error);
391     }
392 
393     /**
394      * Reads the remote host name from the james:remotehost property.
395      *
396      * @param node mail node
397      * @return remote host name, or <code>null</code> if not set
398      * @throws RepositoryException if a repository error occurs
399      */
400     private String getRemoteHost(Node node) throws RepositoryException {
401         try {
402             return node.getProperty("james:remotehost").getString();
403         } catch (PathNotFoundException e) {
404             return null;
405         }
406     }
407 
408     /**
409      * Writes the remote host name to the james:remotehost property.
410      *
411      * @param node mail node
412      * @param host remote host name
413      * @throws RepositoryException if a repository error occurs
414      */
415     private void setRemoteHost(Node node, String host)
416             throws RepositoryException {
417         node.setProperty("james:remotehost", host);
418     }
419 
420     /**
421      * Reads the remote address from the james:remoteaddr property.
422      *
423      * @param node mail node
424      * @return remote address, or <code>null</code> if not set
425      * @throws RepositoryException if a repository error occurs
426      */
427     private String getRemoteAddr(Node node) throws RepositoryException {
428         try {
429             return node.getProperty("james:remoteaddr").getString();
430         } catch (PathNotFoundException e) {
431             return null;
432         }
433     }
434 
435     /**
436      * Writes the remote address to the james:remoteaddr property.
437      *
438      * @param node mail node
439      * @param addr remote address
440      * @throws RepositoryException if a repository error occurs
441      */
442     private void setRemoteAddr(Node node, String addr)
443             throws RepositoryException {
444         node.setProperty("james:remoteaddr", addr);
445     }
446 
447     /**
448      * Reads the envelope sender from the james:sender property.
449      *
450      * @param node mail node
451      * @return envelope sender, or <code>null</code> if not set
452      * @throws MessagingException if a messaging error occurs
453      * @throws RepositoryException if a repository error occurs
454      */
455     private MailAddress getSender(Node node)
456             throws MessagingException, RepositoryException {
457         try {
458             String sender = node.getProperty("james:sender").getString();
459             return new MailAddress(sender);
460         } catch (PathNotFoundException e) {
461             return null;
462         }
463     }
464 
465     /**
466      * Writes the envelope sender to the james:sender property.
467      *
468      * @param node mail node
469      * @param sender envelope sender
470      * @throws MessagingException if a messaging error occurs
471      * @throws RepositoryException if a repository error occurs
472      */
473     private void setSender(Node node, MailAddress sender)
474             throws MessagingException, RepositoryException {
475         node.setProperty("james:sender", sender.toString());
476     }
477 
478     /**
479      * Reads the list of recipients from the james:recipients property.
480      *
481      * @param node mail node
482      * @return list of recipient, or an empty list if not set
483      * @throws MessagingException if a messaging error occurs
484      * @throws RepositoryException if a repository error occurs
485      */
486     private Collection getRecipients(Node node)
487             throws MessagingException, RepositoryException {
488         try {
489             Value[] values = node.getProperty("james:recipients").getValues();
490             Collection recipients = new ArrayList(values.length);
491             for (int i = 0; i < values.length; i++) {
492                 recipients.add(new MailAddress(values[i].getString()));
493             }
494             return recipients;
495         } catch (PathNotFoundException e) {
496             return Collections.EMPTY_LIST;
497         }
498     }
499 
500     /**
501      * Writes the list of recipients to the james:recipients property.
502      *
503      * @param node mail node
504      * @param recipients list of recipient
505      * @throws MessagingException if a messaging error occurs
506      * @throws RepositoryException if a repository error occurs
507      */
508     private void setRecipients(Node node, Collection recipients)
509             throws MessagingException, RepositoryException {
510         String[] values = new String[recipients.size()];
511         Iterator iterator = recipients.iterator();
512         for (int i = 0; iterator.hasNext(); i++) {
513             values[i] = iterator.next().toString();
514         }
515         node.setProperty("james:recipients", values);
516     }
517 
518     /**
519      * Reads the message content from the jcr:content/jcr:data binary property.
520      *
521      * @param node mail node
522      * @return mail message
523      * @throws MessagingException if a messaging error occurs
524      * @throws RepositoryException if a repository error occurs
525      * @throws IOException if an IO error occurs
526      */
527     private MimeMessage getMessage(Node node)
528             throws MessagingException, RepositoryException, IOException {
529         try {
530             node = node.getNode("jcr:content");
531         } catch (PathNotFoundException e) {
532             node = node.getProperty("jcr:content").getNode();
533         }
534 
535         InputStream stream = node.getProperty("jcr:data").getStream();
536         try {
537             Properties properties = System.getProperties();
538             return new MimeMessage(
539                     javax.mail.Session.getDefaultInstance(properties),
540                     stream);
541         } finally {
542             stream.close();
543         }
544     }
545 
546     /**
547      * Writes the message content to the jcr:content/jcr:data binary property.
548      *
549      * @param node mail node
550      * @param message mail message
551      * @throws MessagingException if a messaging error occurs
552      * @throws RepositoryException if a repository error occurs
553      * @throws IOException if an IO error occurs
554      */
555     private void setMessage(Node node, final MimeMessage message)
556             throws MessagingException, RepositoryException, IOException {
557         try {
558             node = node.getNode("jcr:content");
559         } catch (PathNotFoundException e) {
560             node = node.getProperty("jcr:content").getNode();
561         }
562 
563         PipedInputStream input = new PipedInputStream();
564         final PipedOutputStream output = new PipedOutputStream(input);
565         new Thread() {
566             public void run() {
567                 try {
568                     message.writeTo(output);
569                 } catch (Exception e) {
570                 } finally {
571                     try {
572                         output.close();
573                     } catch (IOException e) {
574                     }
575                 }
576             }
577         }.start();
578         node.setProperty("jcr:data", input);
579     }
580 
581     /**
582      * Writes the mail attributes from the jamesattr:* property.
583      *
584      * @param node mail node
585      * @param mail mail message
586      * @throws RepositoryException if a repository error occurs
587      * @throws IOException if an IO error occurs
588      */
589     private void getAttributes(Node node, Mail mail)
590             throws RepositoryException, IOException {
591         PropertyIterator iterator = node.getProperties("jamesattr:*");
592         while (iterator.hasNext()) {
593             Property property = iterator.nextProperty();
594             String name = Text.unescapeIllegalJcrChars(
595                     property.getName().substring("jamesattr:".length()));
596             if (property.getType() == PropertyType.BINARY) {
597                 InputStream input = property.getStream();
598                 try {
599                     ObjectInputStream stream = new ObjectInputStream(input);
600                     mail.setAttribute(name, (Serializable) stream.readObject());
601                 } catch (ClassNotFoundException e) {
602                     throw new IOException(e.getMessage());
603                 } finally {
604                     input.close();
605                 }
606             } else {
607                 mail.setAttribute(name, property.getString());
608             }
609         }
610     }
611     
612     /**
613      * Writes the mail attributes to the jamesattr:* property.
614      *
615      * @param node mail node
616      * @param mail mail message
617      * @throws RepositoryException if a repository error occurs
618      * @throws IOException if an IO error occurs
619      */
620     private void setAttributes(Node node, Mail mail)
621             throws RepositoryException, IOException {
622         Iterator iterator = mail.getAttributeNames();
623         while (iterator.hasNext()) {
624             String name = (String) iterator.next();
625             Object value = mail.getAttribute(name);
626             name = "jamesattr:" + Text.escapeIllegalJcrChars(name);
627             if (value instanceof String || value == null) {
628                 node.setProperty(name, (String) value);
629             } else {
630                 ByteArrayOutputStream buffer = new ByteArrayOutputStream();
631                 ObjectOutputStream output = new ObjectOutputStream(buffer);
632                 output.writeObject(value);
633                 output.close();
634                 node.setProperty(
635                         name,
636                         new ByteArrayInputStream(buffer.toByteArray()));
637             }
638         }
639     }
640 
641 }