001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019package org.apache.james.mailbox.maildir.mail;
020
021import java.io.File;
022import java.io.FileFilter;
023import java.io.FilenameFilter;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.regex.Pattern;
028
029import org.apache.commons.io.FileUtils;
030import org.apache.james.mailbox.MailboxSession;
031import org.apache.james.mailbox.exception.MailboxException;
032import org.apache.james.mailbox.exception.MailboxExistsException;
033import org.apache.james.mailbox.exception.MailboxNotFoundException;
034import org.apache.james.mailbox.maildir.MaildirFolder;
035import org.apache.james.mailbox.maildir.MaildirMessageName;
036import org.apache.james.mailbox.maildir.MaildirStore;
037import org.apache.james.mailbox.model.MailboxConstants;
038import org.apache.james.mailbox.model.MailboxPath;
039import org.apache.james.mailbox.store.mail.MailboxMapper;
040import org.apache.james.mailbox.store.mail.model.Mailbox;
041import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox;
042import org.apache.james.mailbox.store.transaction.NonTransactionalMapper;
043
044public class MaildirMailboxMapper extends NonTransactionalMapper implements MailboxMapper<Integer> {
045
046    /**
047     * The {@link MaildirStore} the mailboxes reside in
048     */
049    private final MaildirStore maildirStore;
050    
051    /**
052     * A request-scoped list of mailboxes in order to refer to them via id
053     */
054    private ArrayList<Mailbox<Integer>> mailboxCache = new ArrayList<Mailbox<Integer>>();
055
056    private final MailboxSession session;
057    
058    public MaildirMailboxMapper(MaildirStore maildirStore, MailboxSession session) {
059        this.maildirStore = maildirStore;
060        this.session = session;
061    }
062
063    /**
064     * @see org.apache.james.mailbox.store.mail.MailboxMapper#delete(org.apache.james.mailbox.store.mail.model.Mailbox)
065     */
066    @Override
067    public void delete(Mailbox<Integer> mailbox) throws MailboxException {
068        
069        String folderName = maildirStore.getFolderName(mailbox);
070        File folder = new File(folderName);
071        if (folder.isDirectory()) {
072            try {
073                if (mailbox.getName().equals(MailboxConstants.INBOX)) {
074                    // We must only delete cur, new, tmp and metadata for top INBOX mailbox.
075                    FileUtils.deleteDirectory(new File(folder, MaildirFolder.CUR));
076                    FileUtils.deleteDirectory(new File(folder, MaildirFolder.NEW));
077                    FileUtils.deleteDirectory(new File(folder, MaildirFolder.TMP));
078                    File uidListFile = new File(folder, MaildirFolder.UIDLIST_FILE);
079                    uidListFile.delete();
080                    File validityFile = new File(folder, MaildirFolder.VALIDITY_FILE);
081                    validityFile.delete();
082                }
083                else {
084                    // We simply delete all the folder for non INBOX mailboxes.
085                    FileUtils.deleteDirectory(folder);
086                }
087            } catch (IOException e) {
088                e.printStackTrace();
089                throw new MailboxException("Unable to delete Mailbox " + mailbox, e);
090            }
091        }
092        else
093            throw new MailboxNotFoundException(mailbox.getName());
094    }
095
096   
097    /**
098     * @see org.apache.james.mailbox.store.mail.MailboxMapper#findMailboxByPath(org.apache.james.mailbox.model.MailboxPath)
099     */
100    @Override
101    public Mailbox<Integer> findMailboxByPath(MailboxPath mailboxPath)
102            throws MailboxException, MailboxNotFoundException {      
103        Mailbox<Integer> mailbox = maildirStore.loadMailbox(session, mailboxPath);
104        return cacheMailbox(mailbox);
105    }
106    
107    /**
108     * @see org.apache.james.mailbox.store.mail.MailboxMapper#findMailboxWithPathLike(org.apache.james.mailbox.model.MailboxPath)
109     */
110    @Override
111    public List<Mailbox<Integer>> findMailboxWithPathLike(MailboxPath mailboxPath)
112            throws MailboxException {
113        final Pattern searchPattern = Pattern.compile("[" + MaildirStore.maildirDelimiter + "]"
114                + mailboxPath.getName().replace(".", "\\.").replace(MaildirStore.WILDCARD, ".*"));
115        FilenameFilter filter = MaildirMessageName.createRegexFilter(searchPattern);
116        File root = maildirStore.getMailboxRootForUser(mailboxPath.getUser());
117        File[] folders = root.listFiles(filter);
118        ArrayList<Mailbox<Integer>> mailboxList = new ArrayList<Mailbox<Integer>>();
119        for (File folder : folders)
120            if (folder.isDirectory()) {
121                Mailbox<Integer> mailbox = maildirStore.loadMailbox(session, root, mailboxPath.getNamespace(), mailboxPath.getUser(), folder.getName());
122                mailboxList.add(cacheMailbox(mailbox));
123            }
124        // INBOX is in the root of the folder
125        if (Pattern.matches(mailboxPath.getName().replace(MaildirStore.WILDCARD, ".*"), MailboxConstants.INBOX)) {
126            Mailbox<Integer> mailbox = maildirStore.loadMailbox(session, root, mailboxPath.getNamespace(), mailboxPath.getUser(), "");
127            mailboxList.add(0, cacheMailbox(mailbox));
128        }
129        return mailboxList;
130    }
131
132    /**
133     * @see org.apache.james.mailbox.store.mail.MailboxMapper#hasChildren(org.apache.james.mailbox.store.mail.model.Mailbox, char)
134     */
135    @Override
136    public boolean hasChildren(Mailbox<Integer> mailbox, char delimiter) throws MailboxException, MailboxNotFoundException {
137        String searchString = mailbox.getName() + MaildirStore.maildirDelimiter + MaildirStore.WILDCARD;
138        List<Mailbox<Integer>> mailboxes = findMailboxWithPathLike(
139                new MailboxPath(mailbox.getNamespace(), mailbox.getUser(), searchString));
140        return (mailboxes.size() > 0);
141    }
142
143    /**
144     * @see org.apache.james.mailbox.store.mail.MailboxMapper#save(org.apache.james.mailbox.store.mail.model.Mailbox)
145     */
146    @Override
147    public void save(Mailbox<Integer> mailbox) throws MailboxException {
148        try {
149            Mailbox<Integer> originalMailbox = getCachedMailbox(mailbox.getMailboxId());
150            MaildirFolder folder = maildirStore.createMaildirFolder(mailbox);
151            // equals with null check
152            if (originalMailbox.getName() == null ? mailbox.getName() != null : !originalMailbox.getName().equals(mailbox.getName())) {
153                if (folder.exists())
154                    throw new MailboxExistsException(mailbox.getName());
155                
156                MaildirFolder originalFolder = maildirStore.createMaildirFolder(originalMailbox);
157                // renaming the INBOX means to move its contents to the new folder 
158                if (originalMailbox.getName().equals(MailboxConstants.INBOX)) {
159                    try {
160                        File inboxFolder = originalFolder.getRootFile();
161                        File newFolder = folder.getRootFile();
162                        if (!newFolder.mkdirs())
163                            throw new IOException("Could not create folder " + newFolder);
164                        if (!originalFolder.getCurFolder().renameTo(folder.getCurFolder()))
165                            throw new IOException("Could not rename folder " + originalFolder.getCurFolder() + " to " + folder.getCurFolder());
166                        if (!originalFolder.getNewFolder().renameTo(folder.getNewFolder()))
167                            throw new IOException("Could not rename folder " + originalFolder.getNewFolder() + " to " + folder.getNewFolder());
168                        if (!originalFolder.getTmpFolder().renameTo(folder.getTmpFolder()))
169                            throw new IOException("Could not rename folder " + originalFolder.getTmpFolder() + " to " + folder.getTmpFolder());
170                        File oldUidListFile = new File(inboxFolder, MaildirFolder.UIDLIST_FILE);
171                        File newUidListFile = new File(newFolder, MaildirFolder.UIDLIST_FILE);
172                        if (!oldUidListFile.renameTo(newUidListFile))
173                            throw new IOException("Could not rename file " + oldUidListFile + " to " + newUidListFile);
174                        File oldValidityFile = new File(inboxFolder, MaildirFolder.VALIDITY_FILE);
175                        File newValidityFile = new File(newFolder, MaildirFolder.VALIDITY_FILE);
176                        if (!oldValidityFile.renameTo(newValidityFile))
177                            throw new IOException("Could not rename file " + oldValidityFile + " to " + newValidityFile);
178                        // recreate the INBOX folders, uidvalidity and uidlist will
179                        // automatically be recreated later
180                        if (!originalFolder.getCurFolder().mkdir())
181                            throw new IOException("Could not create folder " + originalFolder.getCurFolder());
182                        if (!originalFolder.getNewFolder().mkdir())
183                            throw new IOException("Could not create folder " + originalFolder.getNewFolder());
184                        if (!originalFolder.getTmpFolder().mkdir())
185                            throw new IOException("Could not create folder " + originalFolder.getTmpFolder());
186                    } catch (IOException e) {
187                        throw new MailboxException("Failed to save Mailbox " + mailbox, e);
188                    }
189                }
190                else {
191                    if (!originalFolder.getRootFile().renameTo(folder.getRootFile()))
192                        throw new MailboxException("Failed to save Mailbox " + mailbox, 
193                                new IOException("Could not rename folder " + originalFolder));
194                }
195            }
196        } catch (MailboxNotFoundException e) {
197            // it cannot be found and is thus new
198            MaildirFolder folder = maildirStore.createMaildirFolder(mailbox);
199            if (!folder.exists()) {
200                boolean success = folder.getRootFile().exists();
201                if (!success) success = folder.getRootFile().mkdirs();
202                if (!success)
203                    throw new MailboxException("Failed to save Mailbox " + mailbox);
204                success = folder.getCurFolder().mkdir();
205                success = success && folder.getNewFolder().mkdir();
206                success = success && folder.getTmpFolder().mkdir();
207                if (!success)
208                    throw new MailboxException("Failed to save Mailbox " + mailbox, new IOException("Needed folder structure can not be created"));
209
210            }
211            try {
212                folder.setUidValidity(mailbox.getUidValidity());
213            } catch (IOException ioe) {
214                throw new MailboxException("Failed to save Mailbox " + mailbox, ioe);
215
216            }
217        }
218        
219    }
220
221    /**
222     * @see org.apache.james.mailbox.store.mail.MailboxMapper#list()
223     */
224    @Override
225    public List<Mailbox<Integer>> list() throws MailboxException {
226        
227       File maildirRoot = maildirStore.getMaildirRoot();
228       List<Mailbox<Integer>> mailboxList = new ArrayList<Mailbox<Integer>>();
229        
230       if (maildirStore.getMaildirLocation().endsWith("/" + MaildirStore.PATH_FULLUSER)) {
231           File[] users = maildirRoot.listFiles();
232           visitUsersForMailboxList(null, users, mailboxList);
233           return mailboxList;
234       }
235       
236       File[] domains = maildirRoot.listFiles();
237       for (File domain: domains) {
238           File[] users = domain.listFiles();
239           visitUsersForMailboxList(domain, users, mailboxList);
240       }
241       return mailboxList;
242        
243    }
244
245    /**
246     * @see org.apache.james.mailbox.store.transaction.TransactionalMapper#endRequest()
247     */
248    @Override
249    public void endRequest() {
250        mailboxCache.clear();
251    }
252    
253    /**
254     * Stores a copy of a mailbox in a cache valid for one request. This is to enable
255     * referring to renamed mailboxes via id.
256     * @param mailbox The mailbox to cache
257     * @return The id of the cached mailbox
258     */
259    private Mailbox<Integer> cacheMailbox(Mailbox<Integer> mailbox) {
260        mailboxCache.add(new SimpleMailbox<Integer>(mailbox));
261        int id = mailboxCache.size() - 1;
262        ((SimpleMailbox<Integer>) mailbox).setMailboxId(id);
263        return mailbox;
264    }
265    
266    /**
267     * Retrieves a mailbox from the cache
268     * @param mailboxId The id of the mailbox to retrieve
269     * @return The mailbox
270     * @throws MailboxNotFoundException If the mailboxId is not in the cache
271     */
272    private Mailbox<Integer> getCachedMailbox(Integer mailboxId) throws MailboxNotFoundException {
273        if (mailboxId == null)
274            throw new MailboxNotFoundException("null");
275        try {
276            return mailboxCache.get(mailboxId);
277        } catch (IndexOutOfBoundsException e) {
278            throw new MailboxNotFoundException(String.valueOf(mailboxId));
279        }
280    }
281    
282    private void visitUsersForMailboxList(File domain, File[] users, List<Mailbox<Integer>> mailboxList) throws MailboxException {
283        
284        String userName = null;
285        
286        for (File user: users) {
287            
288            
289            if (domain == null) {
290                userName = user.getName();
291            }
292            else {
293                userName = user.getName() + "@" + domain.getName();
294            }
295            
296            // Special case for INBOX: Let's use the user's folder.
297            MailboxPath inboxMailboxPath = new MailboxPath(session.getPersonalSpace(), userName, MailboxConstants.INBOX);
298            mailboxList.add(maildirStore.loadMailbox(session, inboxMailboxPath));
299            
300            // List all INBOX sub folders.
301            
302            File[] mailboxes = user.listFiles(new FileFilter() {
303                @Override
304                public boolean accept(File pathname) {
305                    return pathname.getName().startsWith(".");
306                }
307            });
308            
309            for (File mailbox: mailboxes) {
310               
311                
312                MailboxPath mailboxPath = new MailboxPath(MailboxConstants.USER_NAMESPACE, 
313                        userName, 
314                        mailbox.getName().substring(1));
315                mailboxList.add(maildirStore.loadMailbox(session, mailboxPath));
316
317            }
318
319        }
320        
321    }
322
323}