001 /*
002 * Copyright 2010 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package com.hs.mail.imap.mailbox;
017
018 import java.io.File;
019 import java.io.IOException;
020 import java.util.ArrayList;
021 import java.util.Date;
022 import java.util.HashMap;
023 import java.util.List;
024 import java.util.Map;
025 import java.util.Set;
026
027 import javax.mail.Flags;
028
029 import net.sf.ehcache.Ehcache;
030
031 import org.apache.commons.collections.CollectionUtils;
032 import org.apache.commons.io.FileUtils;
033 import org.apache.log4j.Logger;
034 import org.springframework.beans.factory.DisposableBean;
035 import org.springframework.dao.DataAccessException;
036 import org.springframework.transaction.PlatformTransactionManager;
037 import org.springframework.transaction.TransactionStatus;
038 import org.springframework.transaction.support.TransactionCallback;
039 import org.springframework.transaction.support.TransactionCallbackWithoutResult;
040 import org.springframework.transaction.support.TransactionTemplate;
041 import org.springframework.util.Assert;
042
043 import com.hs.mail.container.config.Config;
044 import com.hs.mail.imap.dao.DaoFactory;
045 import com.hs.mail.imap.dao.MailboxDao;
046 import com.hs.mail.imap.dao.MessageDao;
047 import com.hs.mail.imap.dao.SearchDao;
048 import com.hs.mail.imap.event.EventDispatcher;
049 import com.hs.mail.imap.event.EventListener;
050 import com.hs.mail.imap.message.FetchData;
051 import com.hs.mail.imap.message.MailMessage;
052 import com.hs.mail.imap.message.PhysMessage;
053 import com.hs.mail.imap.message.search.SearchKey;
054 import com.hs.mail.util.EhCacheWrapper;
055
056 /**
057 *
058 * @author WonChul Doh
059 * @since Feb 2, 2010
060 *
061 */
062 public class DefaultMailboxManager implements MailboxManager, DisposableBean {
063
064 private static Logger logger = Logger.getLogger(DefaultMailboxManager.class);
065
066 private TransactionTemplate transactionTemplate;
067 private EhCacheWrapper<Long, FetchData> fdCache;
068 private EhCacheWrapper<Long, Map<String, String>> hdCache;
069 private EventDispatcher eventDispatcher = new EventDispatcher();
070
071 public void setTransactionManager(
072 PlatformTransactionManager transactionManager) {
073 Assert.notNull(transactionManager,
074 "The 'transactionManager' argument must not be null.");
075 this.transactionTemplate = new TransactionTemplate(transactionManager);
076 }
077
078 public TransactionTemplate getTransactionTemplate() {
079 return transactionTemplate;
080 }
081
082 public void setFetchDataCache(Ehcache cache) {
083 this.fdCache = new EhCacheWrapper<Long, FetchData>(cache);
084 }
085
086 public void setHeaderCache(Ehcache cache) {
087 this.hdCache = new EhCacheWrapper<Long, Map<String, String>>(cache);
088 }
089
090 public EventDispatcher getEventDispatcher() {
091 return eventDispatcher;
092 }
093
094 public void addEventListener(EventListener listener) {
095 if (listener != null)
096 eventDispatcher.addEventListener(listener);
097 }
098
099 public void removeEventListener(EventListener listener) {
100 if (listener != null)
101 eventDispatcher.removeEventListener(listener);
102 }
103
104 public Mailbox getMailbox(long ownerID, String mailboxName) {
105 MailboxDao dao = DaoFactory.getMailboxDao();
106 return dao.getMailbox(ownerID, mailboxName);
107 }
108
109 public boolean mailboxExists(long ownerID, String mailboxName) {
110 MailboxDao dao = DaoFactory.getMailboxDao();
111 return dao.mailboxExists(ownerID, mailboxName);
112 }
113
114 public List<Mailbox> getChildren(long userID, long ownerID,
115 String mailboxName, boolean subscribed) {
116 MailboxDao dao = DaoFactory.getMailboxDao();
117 return dao.getChildren(userID, ownerID, mailboxName, subscribed);
118 }
119
120 public List<Long> getMailboxIDList(String mailboxName) {
121 MailboxDao dao = DaoFactory.getMailboxDao();
122 return dao.getMailboxIDList(mailboxName);
123 }
124
125 public boolean hasChildren(Mailbox mailbox) {
126 MailboxDao dao = DaoFactory.getMailboxDao();
127 return dao.getChildCount(mailbox.getOwnerID(), mailbox.getName()) > 0;
128 }
129
130 public List<Long> expunge(long mailboxID) {
131 MailboxDao dao = DaoFactory.getMailboxDao();
132 return dao.getDeletedMessageIDList(mailboxID);
133 }
134
135 public List<Long> search(UidToMsnMapper map, long mailboxID, SearchKey key) {
136 SearchDao dao = DaoFactory.getSearchDao();
137 return dao.query(map, mailboxID, key);
138 }
139
140 public Mailbox createMailbox(final long ownerID, final String mailboxName) {
141 return (Mailbox) getTransactionTemplate().execute(
142 new TransactionCallback() {
143 public Object doInTransaction(TransactionStatus status) {
144 try {
145 MailboxDao dao = DaoFactory.getMailboxDao();
146 return dao.createMailbox(ownerID, mailboxName);
147 } catch (DataAccessException ex) {
148 status.setRollbackOnly();
149 throw ex;
150 }
151 }
152 });
153 }
154
155 public void renameMailbox(final Mailbox source, final String targetName) {
156 getTransactionTemplate().execute(
157 new TransactionCallbackWithoutResult() {
158 public void doInTransactionWithoutResult(
159 TransactionStatus status) {
160 try {
161 MailboxDao dao = DaoFactory.getMailboxDao();
162 dao.renameMailbox(source, targetName);
163 } catch (DataAccessException ex) {
164 status.setRollbackOnly();
165 throw ex;
166 }
167 }
168 });
169 }
170
171 public void deleteMailbox(final long ownerID, final long mailboxID,
172 final boolean delete) {
173 getTransactionTemplate().execute(
174 new TransactionCallbackWithoutResult() {
175 public void doInTransactionWithoutResult(
176 TransactionStatus status) {
177 try {
178 MailboxDao dao = DaoFactory.getMailboxDao();
179 List<PhysMessage> danglings = dao
180 .getDanglingMessageIDList(ownerID,
181 mailboxID);
182 // We must evict objects for these deleted messages.
183 // But, how we can do it?
184 dao.deleteMessages(ownerID, mailboxID);
185 if (delete) {
186 dao.deleteMailbox(ownerID, mailboxID);
187 } else {
188 // prevent the mailbox from selecting
189 dao.forbidSelectMailbox(ownerID, mailboxID);
190 }
191 if (CollectionUtils.isNotEmpty(danglings)) {
192 for (PhysMessage pm : danglings) {
193 hdCache.remove(pm.getPhysMessageID());
194 deletePhysicalMessage(pm);
195 }
196 }
197 } catch (DataAccessException ex) {
198 status.setRollbackOnly();
199 throw ex;
200 }
201 }
202 });
203 }
204
205 private void deletePhysicalMessage(PhysMessage pm) {
206 MessageDao dao = DaoFactory.getMessageDao();
207 dao.deletePhysicalMessage(pm.getPhysMessageID());
208 hdCache.remove(pm.getPhysMessageID());
209 try {
210 File file = Config.getDataFile(pm.getInternalDate(), pm.getPhysMessageID());
211 FileUtils.forceDelete(file);
212 } catch (IOException ex) {
213 logger.warn(ex.getMessage(), ex); // Ignore - What we can do?
214 }
215 }
216
217 public boolean isSubscribed(long userID, String mailboxName) {
218 MailboxDao dao = DaoFactory.getMailboxDao();
219 return dao.isSubscribed(userID, mailboxName);
220 }
221
222 public void addSubscription(final long userID, final long mailboxID,
223 final String mailboxName) {
224 getTransactionTemplate().execute(
225 new TransactionCallbackWithoutResult() {
226 public void doInTransactionWithoutResult(
227 TransactionStatus status) {
228 try {
229 MailboxDao dao = DaoFactory.getMailboxDao();
230 dao.addSubscription(userID, mailboxID, mailboxName);
231 } catch (DataAccessException ex) {
232 status.setRollbackOnly();
233 throw ex;
234 }
235 }
236 });
237 }
238
239 public void deleteSubscription(final long userID, final String mailboxName) {
240 getTransactionTemplate().execute(
241 new TransactionCallbackWithoutResult() {
242 public void doInTransactionWithoutResult(
243 TransactionStatus status) {
244 try {
245 MailboxDao dao = DaoFactory.getMailboxDao();
246 dao.deleteSubscription(userID, mailboxName);
247 } catch (DataAccessException ex) {
248 status.setRollbackOnly();
249 throw ex;
250 }
251 }
252 });
253 }
254
255 public FetchData getMessageFetchData(long uid) {
256 FetchData fd = fdCache.get(uid);
257 if (fd == null) {
258 MessageDao dao = DaoFactory.getMessageDao();
259 fd = dao.getMessageFetchData(uid);
260 if (fd != null) {
261 fdCache.put(uid, fd);
262 } else {
263 logger.error("Failed to retrieve fetch data for message ["
264 + uid + "]");
265 }
266 }
267 return fd;
268 }
269
270 public Flags getFlags(long uid) {
271 MessageDao dao = DaoFactory.getMessageDao();
272 return dao.getFlags(uid);
273 }
274
275 public List<Long> getMessageIDList(long mailboxID) {
276 MessageDao dao = DaoFactory.getMessageDao();
277 return dao.getMessageIDList(mailboxID);
278 }
279
280 public void addMessage(final long ownerID, final MailMessage message,
281 String mailboxName) {
282 Mailbox mailbox = getMailbox(ownerID, mailboxName);
283 if (mailbox == null) {
284 mailbox = createMailbox(ownerID, mailboxName);
285 }
286 final long mailboxID = mailbox.getMailboxID();
287 getTransactionTemplate().execute(
288 new TransactionCallbackWithoutResult() {
289 public void doInTransactionWithoutResult(
290 TransactionStatus status) {
291 try {
292 MessageDao dao = DaoFactory.getMessageDao();
293 dao.addMessage(mailboxID, message);
294 eventDispatcher.added(mailboxID);
295 } catch (DataAccessException ex) {
296 status.setRollbackOnly();
297 throw ex;
298 }
299 }
300 });
301 }
302
303 public void appendMessage(long mailboxID, Date internalDate, Flags flags,
304 File file) throws IOException {
305 // If a date-time is specified, the internal date SHOULD be set in
306 // the resulting message; otherwise, the internal date of the
307 // resulting message is set to the current date and time by default.
308 if (internalDate == null) {
309 internalDate = new Date();
310 }
311
312 // If a flag parenthesized list is specified, the flags SHOULD be
313 // set in the resulting message; otherwise, the flag list of the
314 // resulting message is set to empty by default.
315 if (flags == null) {
316 flags = new Flags();
317 }
318
319 MailMessage message = MailMessage.createMailMessage(file, internalDate,
320 flags);
321 appendMessage(mailboxID, message);
322
323 // Save the message file
324 message.save(true);
325 }
326
327 private long appendMessage(final long mailboxID, final MailMessage message) {
328 return (Long) getTransactionTemplate().execute(
329 new TransactionCallback() {
330 public Object doInTransaction(TransactionStatus status) {
331 try {
332 MessageDao dao = DaoFactory.getMessageDao();
333 dao.addMessage(mailboxID, message, message.getFlags());
334 eventDispatcher.added(mailboxID);
335 return message.getPhysMessageID();
336 } catch (DataAccessException ex) {
337 status.setRollbackOnly();
338 throw ex;
339 }
340 }
341 });
342 }
343
344 public void deleteMessage(final long uid) {
345 fdCache.remove(uid);
346 getTransactionTemplate().execute(
347 new TransactionCallbackWithoutResult() {
348 public void doInTransactionWithoutResult(
349 TransactionStatus status) {
350 try {
351 MessageDao dao = DaoFactory.getMessageDao();
352 PhysMessage pm = dao.getDanglingMessageID(uid);
353 dao.deleteMessage(uid);
354 if (pm != null) {
355 deletePhysicalMessage(pm);
356 }
357 } catch (DataAccessException ex) {
358 status.setRollbackOnly();
359 throw ex;
360 }
361 }
362 });
363 }
364
365 public void copyMessage(final long uid, final long mailboxID) {
366 getTransactionTemplate().execute(
367 new TransactionCallbackWithoutResult() {
368 public void doInTransactionWithoutResult(
369 TransactionStatus status) {
370 try {
371 MessageDao dao = DaoFactory.getMessageDao();
372 dao.copyMessage(uid, mailboxID);
373 } catch (DataAccessException ex) {
374 status.setRollbackOnly();
375 throw ex;
376 }
377 }
378 });
379 }
380
381 public void resetRecent(final long mailboxID) {
382 List<Long> recents = (List<Long>) getTransactionTemplate().execute(
383 new TransactionCallback() {
384 public Object doInTransaction(TransactionStatus status) {
385 try {
386 MessageDao dao = DaoFactory.getMessageDao();
387 return dao.resetRecent(mailboxID);
388 } catch (DataAccessException ex) {
389 status.setRollbackOnly();
390 throw ex;
391 }
392 }
393 });
394 if (CollectionUtils.isNotEmpty(recents)) {
395 for (long uid : recents) {
396 fdCache.remove(uid);
397 }
398 }
399 }
400
401 public void setFlags(final long uid, final Flags flags,
402 final boolean replace, final boolean set) {
403 fdCache.remove(uid);
404 getTransactionTemplate().execute(
405 new TransactionCallbackWithoutResult() {
406 public void doInTransactionWithoutResult(
407 TransactionStatus status) {
408 try {
409 MessageDao dao = DaoFactory.getMessageDao();
410 dao.setFlags(uid, flags, replace, set);
411 } catch (DataAccessException ex) {
412 status.setRollbackOnly();
413 throw ex;
414 }
415 }
416 });
417 }
418
419 /**
420 * Gets the headers of the message.
421 *
422 * @param physMessageID
423 * ID of the physical message
424 * @return map containing header name and value entries
425 */
426 public Map<String, String> getHeader(long physMessageID) {
427 MessageDao dao = DaoFactory.getMessageDao();
428 Map<String, String> results = dao.getHeader(physMessageID);
429 if (hdCache.get(physMessageID) == null) {
430 hdCache.put(physMessageID, defaultHeader(results));
431 }
432 return results;
433 }
434
435 private Map<String, String> defaultHeader(Map<String, String> results) {
436 Map<String, String> header = new HashMap<String, String>();
437 if (results != null) {
438 for (String field : Config.getDefaultCacheFields()) {
439 header.put(field, results.get(field));
440 }
441 }
442 return header;
443 }
444
445 public Map<String, String> getHeader(long physMessageID, String[] fields) {
446 Map<String, String> header = hdCache.get(physMessageID);
447 if (header != null) {
448 return getHeaderFromCache(header, physMessageID, fields);
449 } else {
450 return getHeaderFromStorage(physMessageID, fields);
451 }
452 }
453
454 private String[] getCacheFields(String[] fields) {
455 Set<String> defaultCacheFields = Config.getDefaultCacheFields();
456 for (int i = 0; i < fields.length; i++) {
457 if (!defaultCacheFields.contains(fields[i])) {
458 defaultCacheFields.add(fields[i]);
459 }
460 }
461 return defaultCacheFields
462 .toArray(new String[defaultCacheFields.size()]);
463 }
464
465 private Map<String, String> getHeaderFromStorage(long physMessageID,
466 String[] fields) {
467 MessageDao dao = DaoFactory.getMessageDao();
468 String[] cacheFields = getCacheFields(fields);
469 Map<String, String> header = dao.getHeader(physMessageID, cacheFields);
470 if (header != null) {
471 if (header.size() < fields.length) {
472 for (int i = 0; i < fields.length; i++) {
473 if (!header.containsKey(fields[i])) {
474 header.put(fields[i], null);
475 }
476 }
477 }
478 hdCache.put(physMessageID, header);
479 }
480 return header;
481 }
482
483 private Map<String, String> getHeaderFromCache(Map<String, String> header,
484 long physMessageID, String[] fields) {
485 String[] uncachedFields = getUncachedFields(header, fields);
486 if (uncachedFields != null) {
487 MessageDao dao = DaoFactory.getMessageDao();
488 Map<String, String> uncachedHeader = dao.getHeader(physMessageID,
489 uncachedFields);
490 if (uncachedHeader != null) {
491 for (int i = 0; i < uncachedFields.length; i++) {
492 header.put(uncachedFields[i],
493 uncachedHeader.get(uncachedFields[i]));
494 }
495 } else {
496 for (int i = 0; i < uncachedFields.length; i++) {
497 header.put(uncachedFields[i], null);
498 }
499 }
500 }
501 return header;
502 }
503
504 private String[] getUncachedFields(Map<String, String> header, String[] fields) {
505 List<String> uncachedFields = new ArrayList<String>();
506 for (int i = 0; i < fields.length; i++) {
507 if (!header.containsKey(fields[i])) {
508 uncachedFields.add(fields[i]);
509 }
510 }
511 if (uncachedFields.size() > 0)
512 return uncachedFields.toArray(new String[uncachedFields.size()]);
513 else
514 return null;
515 }
516
517 public void destroy() throws Exception {
518 EhCacheWrapper.flush(fdCache);
519 EhCacheWrapper.flush(hdCache);
520 }
521
522 }