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.IOException;
21 import java.io.InputStream;
22 import java.text.SimpleDateFormat;
23 import java.util.Calendar;
24 import java.util.Date;
25
26 import javax.jcr.Node;
27 import javax.jcr.PathNotFoundException;
28 import javax.jcr.RepositoryException;
29 import javax.mail.Address;
30 import javax.mail.BodyPart;
31 import javax.mail.Message;
32 import javax.mail.MessagingException;
33 import javax.mail.Multipart;
34 import javax.mail.Part;
35 import javax.mail.Message.RecipientType;
36 import javax.mail.internet.ContentType;
37 import javax.mail.internet.MimeMessage;
38
39 import org.apache.jackrabbit.util.Text;
40
41 /**
42 * JavaBean that stores messages to a JCR content repository.
43 * <p>
44 * After instantiating this bean you should use the
45 * {@link #setParentNode(Node)} method to specify the root node under
46 * which all messages should be stored. Then you can call
47 * {@link #storeMessage(Message)} to store messages in the repository.
48 * <p>
49 * The created content structure below the given parent node consists
50 * of a date based .../year/month/day tree structure, below which the actual
51 * messages are stored. A stored message consists of an nt:file node whose
52 * name is based on the subject of the message. The jcr:content child of the
53 * nt:file node contains the MIME structure and all relevant headers of the
54 * message. Note that the original message source is <em>not</em> stored,
55 * which means that some of the message information will be lost.
56 * <p>
57 * The messages are stored using the session associated with the specified
58 * parent node. No locking or synchronization is performed, and it is expected
59 * that only one thread writing to the message subtree at any given moment.
60 * You should use JCR locking or some other explicit synchronization mechanism
61 * if you want to have concurrent writes to the message subtree.
62 */
63 public class JCRStoreBean {
64
65 /**
66 * Parent node where the messages are stored.
67 */
68 private Node parent;
69
70 public void setParentNode(Node parent) {
71 this.parent = parent;
72 }
73
74 /**
75 * Stores the given mail message to the content repository.
76 *
77 * @param message mail message
78 * @throws MessagingException if the message could not be read
79 * @throws RepositoryException if the message could not be saved
80 */
81 public void storeMessage(Message message)
82 throws MessagingException, RepositoryException {
83 try {
84 Date date = message.getSentDate();
85 Node year = getOrAddNode(parent, format("yyyy", date), "nt:folder");
86 Node month = getOrAddNode(year, format("mm", date), "nt:folder");
87 Node day = getOrAddNode(month, format("dd", date), "nt:folder");
88 Node node = createNode(day, getMessageName(message), "nt:file");
89 importEntity(message, node);
90 parent.save();
91 } catch (IOException e) {
92 throw new MessagingException("Could not read message", e);
93 }
94 }
95
96 /**
97 * Import the given entity to the given JCR node.
98 *
99 * @param entity the source entity
100 * @param parent the target node
101 * @throws MessagingException if the message could not be read
102 * @throws RepositoryException if the message could not be written
103 * @throws IOException if the message could not be read
104 */
105 private void importEntity(Part entity, Node parent)
106 throws MessagingException, RepositoryException, IOException {
107 Node node = parent.addNode("jcr:content", "nt:unstructured");
108
109 setProperty(node, "description", entity.getDescription());
110 setProperty(node, "disposition", entity.getDisposition());
111 setProperty(node, "filename", entity.getFileName());
112
113 if (entity instanceof MimeMessage) {
114 MimeMessage mime = (MimeMessage) entity;
115 setProperty(node, "subject", mime.getSubject());
116 setProperty(node, "message-id", mime.getMessageID());
117 setProperty(node, "content-id", mime.getContentID());
118 setProperty(node, "content-md5", mime.getContentMD5());
119 setProperty(node, "language", mime.getContentLanguage());
120 setProperty(node, "sent", mime.getSentDate());
121 setProperty(node, "received", mime.getReceivedDate());
122 setProperty(node, "from", mime.getFrom());
123 setProperty(node, "to", mime.getRecipients(RecipientType.TO));
124 setProperty(node, "cc", mime.getRecipients(RecipientType.CC));
125 setProperty(node, "bcc", mime.getRecipients(RecipientType.BCC));
126 setProperty(node, "reply-to", mime.getReplyTo());
127 setProperty(node, "sender", mime.getSender());
128 }
129
130 Object content = entity.getContent();
131 ContentType type = getContentType(entity);
132 node.setProperty("jcr:mimeType", type.getBaseType());
133 if (content instanceof Multipart) {
134 Multipart multipart = (Multipart) content;
135 for (int i = 0; i < multipart.getCount(); i++) {
136 BodyPart part = multipart.getBodyPart(i);
137 Node child;
138 if (part.getFileName() != null) {
139 child = createNode(node, part.getFileName(), "nt:file");
140 } else {
141 child = createNode(node, "part", "nt:unstructured");
142 }
143 importEntity(part, child);
144 }
145 } else if (content instanceof String) {
146 byte[] bytes = ((String) content).getBytes("UTF-8");
147 node.setProperty("jcr:encoding", "UTF-8");
148 node.setProperty("jcr:data", new ByteArrayInputStream(bytes));
149 } else if (content instanceof InputStream) {
150 setProperty(
151 node, "jcr:encoding", type.getParameter("encoding"));
152 node.setProperty("jcr:data", (InputStream) content);
153 } else {
154 node.setProperty("jcr:data", entity.getInputStream());
155 }
156 }
157
158 /**
159 * Formats the given date using the given {@link SimpleDateFormat}
160 * format string.
161 *
162 * @param format format string
163 * @param date date to be formatted
164 * @return formatted date
165 */
166 private String format(String format, Date date) {
167 return new SimpleDateFormat(format).format(date);
168 }
169
170 /**
171 * Suggests a name for the node where the given message will be stored.
172 *
173 * @param message mail message
174 * @return suggested name
175 * @throws MessagingException if an error occurs
176 */
177 private String getMessageName(Message message)
178 throws MessagingException {
179 String name = message.getSubject();
180 if (name == null) {
181 name = "unnamed";
182 } else {
183 name = name.replaceAll("[^A-Za-z0-9 ]", "").trim();
184 if (name.length() == 0) {
185 name = "unnamed";
186 }
187 }
188 return name;
189 }
190
191 /**
192 * Returns the named child node of the given parent. If the child node
193 * does not exist, it is automatically created with the given node type.
194 * The created node is not saved by this method.
195 *
196 * @param parent parent node
197 * @param name name of the child node
198 * @param type type of the child node
199 * @return child node
200 * @throws RepositoryException if the child node could not be accessed
201 */
202 private Node getOrAddNode(Node parent, String name, String type)
203 throws RepositoryException {
204 try {
205 return parent.getNode(name);
206 } catch (PathNotFoundException e) {
207 return parent.addNode(name, type);
208 }
209 }
210
211 /**
212 * Creates a new node with a name that resembles the given suggestion.
213 * The created node is not saved by this method.
214 *
215 * @param parent parent node
216 * @param name suggested name
217 * @param type node type
218 * @return created node
219 * @throws RepositoryException if an error occurs
220 */
221 private Node createNode(Node parent, String name, String type)
222 throws RepositoryException {
223 String original = name;
224 name = Text.escapeIllegalJcrChars(name);
225 for (int i = 2; parent.hasNode(name); i++) {
226 name = Text.escapeIllegalJcrChars(original + i);
227 }
228 return parent.addNode(name, type);
229 }
230
231 /**
232 * Returns the content type of the given message entity. Returns
233 * the default "text/plain" content type if a content type is not
234 * available. Returns "application/octet-stream" if an error occurs.
235 *
236 * @param entity the message entity
237 * @return content type, or <code>text/plain</code> if not available
238 */
239 private static ContentType getContentType(Part entity) {
240 try {
241 String type = entity.getContentType();
242 if (type != null) {
243 return new ContentType(type);
244 } else {
245 return new ContentType("text/plain");
246 }
247 } catch (MessagingException e) {
248 ContentType type = new ContentType();
249 type.setPrimaryType("application");
250 type.setSubType("octet-stream");
251 return type;
252 }
253 }
254
255 /**
256 * Sets the named property if the given value is not null.
257 *
258 * @param node target node
259 * @param name property name
260 * @param value property value
261 * @throws RepositoryException if an error occurs
262 */
263 private void setProperty(Node node, String name, String value)
264 throws RepositoryException {
265 if (value != null) {
266 node.setProperty(name, value);
267 }
268 }
269
270 /**
271 * Sets the named property if the given array of values is
272 * not null or empty.
273 *
274 * @param node target node
275 * @param name property name
276 * @param values property values
277 * @throws RepositoryException if an error occurs
278 */
279 private void setProperty(Node node, String name, String[] values)
280 throws RepositoryException {
281 if (values != null && values.length > 0) {
282 node.setProperty(name, values);
283 }
284 }
285
286 /**
287 * Sets the named property if the given value is not null.
288 *
289 * @param node target node
290 * @param name property name
291 * @param value property value
292 * @throws RepositoryException if an error occurs
293 */
294 private void setProperty(Node node, String name, Date value)
295 throws RepositoryException {
296 if (value != null) {
297 Calendar calendar = Calendar.getInstance();
298 calendar.setTime(value);
299 node.setProperty(name, calendar);
300 }
301 }
302
303 /**
304 * Sets the named property if the given value is not null.
305 *
306 * @param node target node
307 * @param name property name
308 * @param value property value
309 * @throws RepositoryException if an error occurs
310 */
311 private void setProperty(Node node, String name, Address value)
312 throws RepositoryException {
313 if (value != null) {
314 node.setProperty(name, value.toString());
315 }
316 }
317
318 /**
319 * Sets the named property if the given array of values is
320 * not null or empty.
321 *
322 * @param node target node
323 * @param name property name
324 * @param values property values
325 * @throws RepositoryException if an error occurs
326 */
327 private void setProperty(Node node, String name, Address[] values)
328 throws RepositoryException {
329 if (values != null && values.length > 0) {
330 String[] strings = new String[values.length];
331 for (int i = 0; i < values.length; i++) {
332 strings[i] = values[i].toString();
333 }
334 node.setProperty(name, strings);
335 }
336 }
337
338 }