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 }