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    }