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;
020
021import java.io.BufferedReader;
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileNotFoundException;
025import java.io.FileOutputStream;
026import java.io.FileReader;
027import java.io.FilenameFilter;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.InputStreamReader;
031import java.io.OutputStream;
032import java.io.PrintWriter;
033import java.util.ArrayList;
034import java.util.HashMap;
035import java.util.LinkedList;
036import java.util.Map;
037import java.util.Map.Entry;
038import java.util.NoSuchElementException;
039import java.util.Properties;
040import java.util.SortedMap;
041import java.util.TreeMap;
042
043import org.apache.commons.io.IOUtils;
044import org.apache.commons.lang.ArrayUtils;
045import org.apache.james.mailbox.MailboxPathLocker;
046import org.apache.james.mailbox.MailboxPathLocker.LockAwareExecution;
047import org.apache.james.mailbox.exception.MailboxException;
048import org.apache.james.mailbox.model.MailboxACL;
049import org.apache.james.mailbox.model.MailboxPath;
050import org.apache.james.mailbox.model.SimpleMailboxACL;
051import org.apache.james.mailbox.model.MailboxACL.MailboxACLEntryKey;
052import org.apache.james.mailbox.model.MailboxACL.MailboxACLRights;
053import org.apache.james.mailbox.MailboxSession;
054
055public class MaildirFolder {
056
057    public static final String VALIDITY_FILE = "james-uidvalidity";
058    public static final String UIDLIST_FILE = "james-uidlist";
059    public static final String ACL_FILE = "james-acl";
060    public static final String CUR = "cur";
061    public static final String NEW = "new";
062    public static final String TMP = "tmp";
063    
064    private File rootFolder;
065    private File curFolder;
066    private File newFolder;
067    private File tmpFolder;
068    private File uidFile;
069    private File aclFile;
070    
071    private long lastUid = -1;
072    private int messageCount = 0;
073    private long uidValidity = -1;
074    private MailboxACL acl;
075    private boolean messageNameStrictParse = false;
076
077    private final MailboxPathLocker locker;
078
079    private final MailboxPath path;
080    
081    /**
082     * Representation of a maildir folder containing the message folders
083     * and some special files
084     * @param absPath The absolute path of the mailbox folder
085     */
086    public MaildirFolder(String absPath, MailboxPath path, MailboxPathLocker locker) {
087        this.rootFolder = new File(absPath);
088        this.curFolder = new File(rootFolder, CUR);
089        this.newFolder = new File(rootFolder, NEW);
090        this.tmpFolder = new File(rootFolder, TMP);
091        this.uidFile = new File(rootFolder, UIDLIST_FILE);
092        this.aclFile = new File(rootFolder, ACL_FILE);
093        this.locker = locker;
094        this.path = path;
095    }
096
097    private MaildirMessageName newMaildirMessageName(MaildirFolder folder, String fullName) {
098        MaildirMessageName mdn = new MaildirMessageName(folder, fullName);
099        mdn.setMessageNameStrictParse(isMessageNameStrictParse());
100        return mdn;
101    }
102
103    /**
104     * Returns whether the names of message files in this folder are parsed in
105     * a strict manner ({@code true}), which means a size field and flags are
106     * expected.
107     *
108     * @return
109     */
110    public boolean isMessageNameStrictParse() {
111        return messageNameStrictParse;
112    }
113
114    /**
115     * Specifies whether the names of message files in this folder are parsed in
116     * a strict manner ({@code true}), which means a size field and flags are
117     * expected.
118     *
119     * @param messageNameStrictParse
120     */
121    public void setMessageNameStrictParse(boolean messageNameStrictParse) {
122        this.messageNameStrictParse = messageNameStrictParse;
123    }
124
125  /**
126     * Returns the {@link File} of this Maildir folder.
127     * @return the root folder
128     */
129    public File getRootFile() {
130        return rootFolder;
131    }
132
133    /**
134     * Tests whether the directory belonging to this {@link MaildirFolder} exists 
135     * @return true if the directory belonging to this {@link MaildirFolder} exists ; false otherwise 
136     */
137    public boolean exists() {
138        return rootFolder.isDirectory() && curFolder.isDirectory() && newFolder.isDirectory() && tmpFolder.isDirectory();
139    }
140    
141    /**
142     * Checks whether the folder's contents have been changed after
143     * the uidfile has been created.
144     * @return true if the contents have been changed.
145     */
146    private boolean isModified() {
147        long uidListModified = uidFile.lastModified();
148        long curModified = curFolder.lastModified();
149        long newModified = newFolder.lastModified();
150        // because of bad time resolution of file systems we also check "equals"
151        if (curModified >= uidListModified || newModified >= uidListModified) {
152            return true;
153        }
154        return false;
155    }
156    
157    /**
158     * Returns the ./cur folder of this Maildir folder.
159     * @return the <code>./cur</code> folder
160     */
161    public File getCurFolder() {
162        return curFolder;
163    }
164    
165    /**
166     * Returns the ./new folder of this Maildir folder.
167     * @return the <code>./new</code> folder
168     */
169    public File getNewFolder() {
170        return newFolder;
171    }
172    
173    /**
174     * Returns the ./tmp folder of this Maildir folder.
175     * @return the <code>./tmp</code> folder
176     */
177    public File getTmpFolder() {
178        return tmpFolder;
179    }
180    
181    /**
182     * Returns the nextUid value and increases it.
183     * @return nextUid
184     */
185    private long getNextUid() {
186        return ++lastUid;
187    }
188    
189    /**
190     * Returns the last uid used in this mailbox
191     * @param session
192     * @return lastUid
193     * @throws MailboxException
194     */
195    public long getLastUid(MailboxSession session) throws MailboxException {
196        if (lastUid == -1) {
197            readLastUid(session);
198        }
199        return lastUid;
200    }
201    
202    public long getHighestModSeq() throws IOException {
203        long newModified = getNewFolder().lastModified();
204        long curModified = getCurFolder().lastModified();
205        if (newModified  == 0L && curModified == 0L) {
206            throw new IOException("Unable to read highest modSeq");
207        }
208        return Math.max(newModified, curModified);
209    }
210
211    /**
212     * Read the lastUid of the given mailbox from the file system.
213     * 
214     * @param session
215     * @throws MailboxException if there are problems with the uidList file
216     */
217    private void readLastUid(MailboxSession session) throws MailboxException {
218        locker.executeWithLock(session, path, new LockAwareExecution<Void>() {
219            
220            @Override
221            public Void execute() throws MailboxException {
222                File uidList = uidFile;
223                FileReader fileReader = null;
224                BufferedReader reader = null;
225                try {
226                    if (!uidList.exists())
227                        createUidFile();
228                    fileReader = new FileReader(uidList);
229                    reader = new BufferedReader(fileReader);
230                    String line = reader.readLine();
231                    if (line != null)
232                        readUidListHeader(line);
233                    return null;
234                } catch (IOException e) {
235                    throw new MailboxException("Unable to read last uid", e);
236                } finally {
237                    IOUtils.closeQuietly(reader);
238                    IOUtils.closeQuietly(fileReader);
239                }                
240            }
241        }, true);
242        
243        
244    }
245
246    /**
247     * Returns the uidValidity of this mailbox
248     * @return The uidValidity
249     * @throws IOException
250     */
251    public long getUidValidity() throws IOException {
252        if (uidValidity == -1)
253            uidValidity = readUidValidity();
254        return uidValidity;
255    }
256    
257    /**
258     * Sets the uidValidity for this mailbox and writes it to the file system
259     * @param uidValidity
260     * @throws IOException
261     */
262    public void setUidValidity(long uidValidity) throws IOException {
263        saveUidValidity(uidValidity);
264        this.uidValidity = uidValidity;
265    }
266
267    /**
268     * Read the uidValidity of the given mailbox from the file system.
269     * If the respective file is not yet there, it gets created and
270     * filled with a brand new uidValidity.
271     * @return The uidValidity
272     * @throws IOException if there are problems with the validity file
273     */
274    private long readUidValidity() throws IOException {
275        File validityFile = new File(rootFolder, VALIDITY_FILE);
276        if (!validityFile.exists()) {
277            return resetUidValidity();
278        }
279        FileInputStream fis = null;
280        InputStreamReader isr = null;
281        try {
282            fis = new FileInputStream(validityFile);
283            isr = new InputStreamReader(fis);
284            char[] uidValidity = new char[20];
285            int len = isr.read(uidValidity);
286            return Long.parseLong(String.valueOf(uidValidity, 0, len).trim());
287        } finally {
288            IOUtils.closeQuietly(isr);
289            IOUtils.closeQuietly(fis);
290        }
291    }
292
293    /**
294     * Save the given uidValidity to the file system
295     * @param uidValidity
296     * @throws IOException
297     */
298    private void saveUidValidity(long uidValidity) throws IOException {
299        File validityFile = new File(rootFolder, VALIDITY_FILE);
300        if (!validityFile.createNewFile())
301            throw new IOException("Could not create file " + validityFile);
302        FileOutputStream fos = new FileOutputStream(validityFile);
303        try {
304            fos.write(String.valueOf(uidValidity).getBytes());
305        } finally {
306            IOUtils.closeQuietly(fos);
307        }
308    }
309    
310    /**
311     * Sets and returns a new uidValidity for this folder.
312     * @return the new uidValidity
313     * @throws IOException
314     */
315    private long resetUidValidity() throws IOException {
316        // using the timestamp as uidValidity
317        long timestamp = System.currentTimeMillis();
318        setUidValidity(timestamp);
319        return timestamp;
320    }
321    
322    /**
323     * Searches the uid list for a certain uid and returns the according {@link MaildirMessageName}
324     * 
325     * @param session
326     * @param uid The uid to search for
327     * @return The {@link MaildirMessageName} that belongs to the uid
328     * @throws IOException If the uidlist file cannot be found or read
329     */
330    public MaildirMessageName getMessageNameByUid(final MailboxSession session, final Long uid) throws MailboxException {
331       
332        return locker.executeWithLock(session, path, new LockAwareExecution<MaildirMessageName>() {
333            
334            @Override
335            public MaildirMessageName execute() throws MailboxException {
336                FileReader fileReader = null;
337                BufferedReader reader = null;
338                File uidList = uidFile;
339                try {
340                    fileReader = new FileReader(uidList);
341                    reader = new BufferedReader(fileReader);
342                    String uidString = String.valueOf(uid);
343                    String line = reader.readLine(); // the header
344                    int lineNumber = 1;
345                    while ((line = reader.readLine()) != null) {
346                        if (!line.equals("")) {
347                            int gap = line.indexOf(" ");
348                            if (gap == -1) {
349                                // there must be some issues in the file if no gap can be found
350                                session.getLog().info("Corrupted entry in uid-file " + uidList + " line " + lineNumber++);
351                                continue;
352                            }
353                            
354                            if (line.substring(0, gap).equals(uidString)) {
355                                return newMaildirMessageName(MaildirFolder.this, line.substring(gap + 1));
356                            }
357                        }
358                    }
359                    
360                    // TODO: Is this right!?
361                    return null;
362                } catch (IOException e) {
363                    throw new MailboxException("Unable to read messagename for uid " + uid, e);
364                } finally {
365                    IOUtils.closeQuietly(reader);
366                    IOUtils.closeQuietly(fileReader);
367                }                
368            }
369        }, true);
370    }
371    
372    /**
373     * Reads all uids between the two boundaries from the folder and returns them as
374     * a sorted map together with their corresponding {@link MaildirMessageName}s.
375     *
376     * @param session
377     * @param from The lower uid limit
378     * @param to The upper uid limit. <code>-1</code> disables the upper limit
379     * @return a {@link Map} whith all uids in the given range and associated {@link MaildirMessageName}s
380     * @throws MailboxException if there is a problem with the uid list file
381     */
382    public SortedMap<Long, MaildirMessageName> getUidMap(final MailboxSession session, final long from, final long to)
383    throws MailboxException {
384        return locker.executeWithLock(session, path, new LockAwareExecution<SortedMap<Long, MaildirMessageName>>() {
385            
386            @Override
387            public SortedMap<Long, MaildirMessageName> execute() throws MailboxException {
388                final SortedMap<Long, MaildirMessageName> uidMap = new TreeMap<Long, MaildirMessageName>();
389
390                File uidList = uidFile;
391
392                if (uidList.isFile()) {
393                    if (isModified()) {
394                        try {
395                            uidMap.putAll(truncateMap(updateUidFile(), from, to));
396                        } catch (MailboxException e) {
397                            // weird case if someone deleted the uidlist after
398                            // checking its
399                            // existence and before trying to update it.
400                            uidMap.putAll(truncateMap(createUidFile(), from, to));
401                        }
402                    } else {
403                        // the uidList is up to date
404                        uidMap.putAll(readUidFile(session, from, to));
405                    }
406                } else {
407                    // the uidList does not exist
408                    uidMap.putAll(truncateMap(createUidFile(), from, to));
409                }
410                return uidMap;
411            }
412        }, true);
413    }
414    
415    public SortedMap<Long, MaildirMessageName> getUidMap(MailboxSession session, FilenameFilter filter, long from, long to)
416    throws MailboxException {
417        SortedMap<Long, MaildirMessageName> allUids = getUidMap(session, from, to);
418        SortedMap<Long, MaildirMessageName> filteredUids = new TreeMap<Long, MaildirMessageName>();
419        for (Entry<Long, MaildirMessageName> entry : allUids.entrySet()) {
420            if (filter.accept(null, entry.getValue().getFullName()))
421                filteredUids.put(entry.getKey(), entry.getValue());
422        }
423        return filteredUids;
424    }
425    
426    /**
427     * Reads all uids from the uid list file which match the given filter
428     * and returns as many of them as a sorted map as the limit specifies.
429     * 
430     * 
431     * @param session
432     * @param filter The file names of all returned items match the filter. 
433     * The dir argument to {@link FilenameFilter}.accept(dir, name) will always be null.
434     * @param limit The number of items; a limit smaller then 1 disables the limit
435     * @return A {@link Map} with all uids and associated {@link MaildirMessageName}s
436     * @throws MailboxException if there is a problem with the uid list file
437     */
438    public SortedMap<Long, MaildirMessageName> getUidMap(MailboxSession session, FilenameFilter filter, int limit) throws MailboxException {
439        SortedMap<Long, MaildirMessageName> allUids = getUidMap(session, 0, -1);
440        SortedMap<Long, MaildirMessageName> filteredUids = new TreeMap<Long, MaildirMessageName>();
441        int theLimit = limit;
442        if (limit < 1)
443            theLimit = allUids.size();
444        int counter = 0;
445        for (Entry<Long, MaildirMessageName> entry : allUids.entrySet()) {
446            if (counter >= theLimit)
447                break;
448            if (filter.accept(null, entry.getValue().getFullName())) {
449                filteredUids.put(entry.getKey(), entry.getValue());
450                counter++;
451            }
452        }
453        return filteredUids;
454    }
455    
456    /**
457     * Creates a map of recent messages.
458     * 
459     * @param session
460     * @return A {@link Map} with all uids and associated {@link MaildirMessageName}s of recent messages
461     * @throws MailboxException If there is a problem with the uid list file
462     */
463    public SortedMap<Long, MaildirMessageName> getRecentMessages(final MailboxSession session) throws MailboxException {
464        final String[] recentFiles = getNewFolder().list();
465        final LinkedList<String> lines = new LinkedList<String>();
466        final int theLimit = recentFiles.length;
467        return locker.executeWithLock(session, path, new LockAwareExecution<SortedMap<Long, MaildirMessageName>>() {
468            
469            @Override
470            public SortedMap<Long, MaildirMessageName> execute() throws MailboxException {
471                final SortedMap<Long, MaildirMessageName> recentMessages = new TreeMap<Long, MaildirMessageName>();
472
473                File uidList = uidFile;
474
475                try {
476                    if (!uidList.isFile()) {
477                        if (!uidList.createNewFile())
478                            throw new IOException("Could not create file " + uidList);
479                        String[] curFiles = curFolder.list();
480                        String[] newFiles = newFolder.list();
481                        messageCount = curFiles.length + newFiles.length;
482                        String[] allFiles = (String[]) ArrayUtils.addAll(curFiles, newFiles);
483                        for (String file : allFiles)
484                            lines.add(String.valueOf(getNextUid()) + " " + file);
485                        PrintWriter pw = new PrintWriter(uidList);
486                        try {
487                            pw.println(createUidListHeader());
488                            for (String line : lines)
489                            pw.println(line);
490                        } finally {
491                            IOUtils.closeQuietly(pw);
492                        }
493                    }
494                    else {
495                        FileReader fileReader = null;
496                        BufferedReader reader = null;
497                    try {
498                            fileReader = new FileReader(uidList);
499                            reader = new BufferedReader(fileReader);
500                            String line = reader.readLine();
501                            // the first line in the file contains the next uid and message count
502                            while ((line = reader.readLine()) != null)
503                                lines.add(line);
504                        } finally {
505                            IOUtils.closeQuietly(reader);
506                            IOUtils.closeQuietly(fileReader);
507                        }
508                    }
509                    int counter = 0;
510                    String line;
511                    while (counter < theLimit) {
512                        // walk backwards as recent files are supposedly recent
513                        try {
514                            line = lines.removeLast();
515                        } catch (NoSuchElementException e) {
516                            break; // the list is empty
517                        }
518                        if (!line.equals("")) {
519                            int gap = line.indexOf(" ");
520                            if (gap == -1) {
521                                // there must be some issues in the file if no gap can be found
522                                // there must be some issues in the file if no gap can be found
523                                session.getLog().info("Corrupted entry in uid-file " + uidList + " line " + lines.size());
524                                continue;
525                            }
526                            
527                            Long uid = Long.valueOf(line.substring(0, gap));
528                            String name = line.substring(gap + 1, line.length());
529                            for (String recentFile : recentFiles) {
530                                if (recentFile.equals(name)) {
531                                    recentMessages.put(uid, newMaildirMessageName(MaildirFolder.this, recentFile));
532                                    counter++;
533                                    break;
534                                }
535                            }
536                        }
537                    }
538                } catch (IOException e) {
539                    throw new MailboxException("Unable to read recent messages", e);
540                }   
541                return recentMessages;
542            }
543        }, true);
544    }
545    
546    
547    /**
548     * Creates and returns a uid map (uid -> {@link MaildirMessageName}) and writes it to the disk
549     * @return The uid map
550     * @throws MailboxException
551     */
552    private Map<Long, MaildirMessageName> createUidFile() throws MailboxException {
553        final Map<Long, MaildirMessageName> uidMap = new TreeMap<Long, MaildirMessageName>();
554        File uidList = uidFile;
555        PrintWriter pw = null;
556        try {
557            if (!uidList.createNewFile())
558                throw new IOException("Could not create file " + uidList);
559            lastUid = 0;
560            String[] curFiles = curFolder.list();
561            String[] newFiles = newFolder.list();
562            messageCount = curFiles.length + newFiles.length;
563            String[] allFiles = (String[]) ArrayUtils.addAll(curFiles, newFiles);
564            for (String file : allFiles)
565                uidMap.put(getNextUid(), newMaildirMessageName(MaildirFolder.this, file));
566            //uidMap = new TreeMap<Long, MaildirMessageName>(uidMap);
567            pw = new PrintWriter(uidList);
568            pw.println(createUidListHeader());
569            for (Entry<Long, MaildirMessageName> entry : uidMap.entrySet())
570                pw.println(String.valueOf(entry.getKey()) + " " + entry.getValue().getFullName());
571        } catch (IOException e) {
572            throw new MailboxException("Unable to create uid file", e);
573        } finally {
574            IOUtils.closeQuietly(pw);
575        }     
576
577        return uidMap;
578    }
579    
580    private Map<Long, MaildirMessageName> updateUidFile() throws MailboxException {
581        final Map<Long, MaildirMessageName> uidMap = new TreeMap<Long, MaildirMessageName>();
582        File uidList = uidFile;
583        String[] curFiles = curFolder.list();
584        String[] newFiles = newFolder.list();
585        messageCount = curFiles.length + newFiles.length;
586        HashMap<String, Long> reverseUidMap = new HashMap<String, Long>(messageCount);
587        FileReader fileReader = null;
588        BufferedReader reader = null;
589        PrintWriter pw = null;
590        try {
591            fileReader = new FileReader(uidList);
592            reader = new BufferedReader(fileReader);
593            String line = reader.readLine();
594            // the first line in the file contains the next uid and message count
595            if (line != null)
596                readUidListHeader(line);
597            int lineNumber = 1;
598            while ((line = reader.readLine()) != null) {
599                if (!line.equals("")) {
600                    int gap = line.indexOf(" ");
601                    if (gap == -1) {
602                        // there must be some issues in the file if no gap can be found
603                        throw new MailboxException("Corrupted entry in uid-file " + uidList + " line " + lineNumber++);
604                    }
605                    Long uid = Long.valueOf(line.substring(0, gap));
606                    String name = line.substring(gap + 1, line.length());
607                    reverseUidMap.put(stripMetaFromName(name), uid);
608                }
609            }
610            String[] allFiles = (String[]) ArrayUtils.addAll(curFiles, newFiles);
611            for (String file : allFiles) {
612                MaildirMessageName messageName = newMaildirMessageName(MaildirFolder.this, file);
613                Long uid = reverseUidMap.get(messageName.getBaseName());
614                if (uid == null)
615                    uid = getNextUid();
616                uidMap.put(uid, messageName);
617            }
618            pw = new PrintWriter(uidList);
619            pw.println(createUidListHeader());
620            for (Entry<Long, MaildirMessageName> entry : uidMap.entrySet())
621                pw.println(String.valueOf(entry.getKey()) + " " + entry.getValue().getFullName());
622        } catch (IOException e) {
623            throw new MailboxException("Unable to update uid file", e);
624        } finally {
625            IOUtils.closeQuietly(pw);
626            IOUtils.closeQuietly(fileReader);
627            IOUtils.closeQuietly(reader);
628        }               
629        return uidMap;
630    }
631
632    private Map<Long, MaildirMessageName> readUidFile(MailboxSession session, final long from, final long to) throws MailboxException {
633        final Map<Long, MaildirMessageName> uidMap = new HashMap<Long, MaildirMessageName>();
634
635        File uidList = uidFile;
636        FileReader fileReader = null;
637        BufferedReader reader = null;
638        try {
639            fileReader = new FileReader(uidList);
640            reader = new BufferedReader(fileReader);
641            String line = reader.readLine();
642            // the first line in the file contains the next uid and message
643            // count
644            if (line != null)
645                readUidListHeader(line);
646            int lineNumber = 1;
647            while ((line = reader.readLine()) != null) {
648                if (!line.equals("")) {
649                    int gap = line.indexOf(" ");
650
651                    if (gap == -1) {
652                        // there must be some issues in the file if no gap can be found
653                        session.getLog().info("Corrupted entry in uid-file " + uidList + " line " + lineNumber++);
654                        continue;
655                    }
656
657                    Long uid = Long.valueOf(line.substring(0, gap));
658                    if (uid >= from) {
659                        if (to != -1 && uid > to)
660                            break;
661                        String name = line.substring(gap + 1, line.length());
662                        uidMap.put(uid, newMaildirMessageName(MaildirFolder.this, name));
663                    }
664                }
665            }
666        } catch (IOException e) {
667            throw new MailboxException("Unable to read uid file", e);
668        } finally {
669            IOUtils.closeQuietly(reader);
670            IOUtils.closeQuietly(fileReader);
671        }
672        messageCount = uidMap.size();
673
674        return uidMap;
675    }
676    
677    /**
678     * Sorts the given map and returns a subset which is constricted by a lower and an upper limit.
679     * @param source The source map
680     * @param from The lower limit
681     * @param to The upper limit; <code>-1</code> disables the upper limit.
682     * @return The sorted subset
683     */
684    private SortedMap<Long, MaildirMessageName> truncateMap(Map<Long, MaildirMessageName> source, long from, long to) {
685        TreeMap<Long, MaildirMessageName> sortedMap;
686        if (source instanceof TreeMap<?, ?>) sortedMap = (TreeMap<Long, MaildirMessageName>) source;
687        else sortedMap = new TreeMap<Long, MaildirMessageName>(source);
688        if (to != -1)
689            return sortedMap.subMap(from, to + 1);
690        return sortedMap.tailMap(from);
691    }
692    
693    /**
694     * Parses the header line in uid list files.
695     * The format is: version lastUid messageCount (e.g. 1 615 273)
696     * @param line The raw header line
697     * @throws IOException
698     */
699    private void readUidListHeader(String line) throws IOException {
700        if (line == null)
701            throw new IOException("Header entry in uid-file is null");
702        int gap1 = line.indexOf(" ");
703        if (gap1 == -1) {
704            // there must be some issues in the file if no gap can be found
705            throw new IOException("Corrupted header entry in uid-file");
706            
707        }
708        int version = Integer.valueOf(line.substring(0, gap1));
709        if (version != 1)
710            throw new IOException("Cannot read uidlists with versions other than 1.");
711        int gap2 = line.indexOf(" ", gap1 + 1);
712        lastUid = Long.valueOf(line.substring(gap1 + 1, gap2));
713        messageCount = Integer.valueOf(line.substring(gap2 + 1, line.length()));
714    }
715    
716    /**
717     * Creates a line to put as a header in the uid list file.
718     * @return the line which ought to be the header
719     */
720    private String createUidListHeader() {
721        return "1 " + String.valueOf(lastUid) + " " + String.valueOf(messageCount);
722    }
723    
724    /**
725     * Takes the name of a message file and returns only the base name.
726     * @param fileName The name of the message file
727     * @return the file name without meta data, the unmodified name if it doesn't have meta data
728     */
729    public static String stripMetaFromName(String fileName) {
730        int end = fileName.indexOf(",S="); // the size
731        if (end == -1)
732            end = fileName.indexOf(":2,"); // the flags
733        if (end == -1)
734            return fileName; // there is no meta data to strip
735        return fileName.substring(0, end);
736    }
737
738    /**
739     * Appends a message to the uidlist and returns its uid.
740     * @param session
741     * @param name The name of the message's file
742     * @return The uid of the message
743     * @throws IOException
744     */
745    public long appendMessage(final MailboxSession session, final String name) throws MailboxException {
746        return locker.executeWithLock(session, path, new LockAwareExecution<Long>() {
747            
748            @Override
749            public Long execute() throws MailboxException {
750                File uidList = uidFile;
751                long uid = -1;
752                FileReader fileReader = null;
753                BufferedReader reader = null;
754                PrintWriter pw = null;
755                try {
756                    if (uidList.isFile()) {
757                        fileReader = new FileReader(uidList);
758                        reader = new BufferedReader(fileReader);
759                        String line = reader.readLine();
760                        // the first line in the file contains the next uid and message count
761                        if (line != null)
762                            readUidListHeader(line);
763                        ArrayList<String> lines = new ArrayList<String>();
764                        while ((line = reader.readLine()) != null)
765                            lines.add(line);
766                        uid = getNextUid();
767                        lines.add(String.valueOf(uid) + " " + name);
768                        messageCount++;
769                        pw = new PrintWriter(uidList);
770                        pw.println(createUidListHeader());
771                        for (String entry : lines)
772                            pw.println(entry);
773                    }
774                    else {
775                        // create the file
776                        if (!uidList.createNewFile())
777                            throw new IOException("Could not create file " + uidList);
778                        String[] curFiles = curFolder.list();
779                        String[] newFiles = newFolder.list();
780                        messageCount = curFiles.length + newFiles.length;
781                        ArrayList<String> lines = new ArrayList<String>();
782                        String[] allFiles = (String[]) ArrayUtils.addAll(curFiles, newFiles);
783                        for (String file : allFiles) {
784                            long theUid = getNextUid();
785                            lines.add(String.valueOf(theUid) + " " + file);
786                            // the listed names already include the message to append
787                            if (file.equals(name))
788                                uid = theUid;
789                        }
790                        pw = new PrintWriter(uidList);
791                        pw.println(createUidListHeader());
792                        for (String line : lines)
793                            pw.println(line);
794                    }
795                } catch (IOException e) {
796                    throw new MailboxException("Unable to append msg", e);
797                } finally {
798                    IOUtils.closeQuietly(pw);
799                    IOUtils.closeQuietly(reader);
800                    IOUtils.closeQuietly(fileReader);
801                }
802                if (uid == -1) {
803                    throw new MailboxException("Unable to append msg");
804                } else {
805                   return uid;
806                }
807            }
808        }, true);
809
810    }
811
812    /**
813     * Updates an entry in the uid list.
814     * @param session
815     * @param uid
816     * @param messageName
817     * @throws MailboxException
818     */
819    public void update(final MailboxSession session, final long uid, final String messageName) throws MailboxException {
820        locker.executeWithLock(session, path, new LockAwareExecution<Void>() {
821            
822            @Override
823            public Void execute() throws MailboxException {
824                File uidList = uidFile;
825                FileReader fileReader = null;
826                BufferedReader reader = null;
827                PrintWriter writer = null;
828                try {
829                    fileReader = new FileReader(uidList);
830                    reader = new BufferedReader(fileReader);
831                    String line = reader.readLine();
832                    readUidListHeader(line);
833                    ArrayList<String> lines = new ArrayList<String>();
834                    while ((line = reader.readLine()) != null) {
835                        if (uid == Long.valueOf(line.substring(0, line.indexOf(" "))))
836                            line = String.valueOf(uid) + " " + messageName;
837                        lines.add(line);
838                    }
839                    writer = new PrintWriter(uidList);
840                    writer.println(createUidListHeader());
841                    for (String entry : lines)
842                        writer.println(entry);
843                } catch (IOException e) {
844                    throw new MailboxException("Unable to update msg with uid " + uid, e);
845                } finally {
846                    IOUtils.closeQuietly(writer);
847                    IOUtils.closeQuietly(reader);
848                    IOUtils.closeQuietly(fileReader);
849                }    
850                return null;
851            }
852        }, true);
853
854    }
855    
856    /**
857     * Retrieves the file belonging to the given uid, deletes it and updates
858     * the uid list.
859     * @param uid The uid of the message to delete
860     * @return The {@link MaildirMessageName} of the deleted message
861     * @throws MailboxException If the file cannot be deleted of there is a problem with the uid list
862     */
863    public MaildirMessageName delete(final MailboxSession session, final long uid) throws MailboxException {        
864        return locker.executeWithLock(session, path, new LockAwareExecution<MaildirMessageName>() {
865            
866            @Override
867            public MaildirMessageName execute() throws MailboxException {
868                File uidList = uidFile;
869                FileReader fileReader = null;
870                BufferedReader reader = null;
871                PrintWriter writer = null;
872                MaildirMessageName deletedMessage = null;
873                try {
874                    fileReader = new FileReader(uidList);
875                    reader = new BufferedReader(fileReader);
876                    readUidListHeader(reader.readLine());
877                    
878                    // It may be possible that message count is 0 so we should better not try to calculate the size of the ArrayList
879                    ArrayList<String> lines = new ArrayList<String>();
880                    String line;
881                    int lineNumber = 1;
882                    while ((line = reader.readLine()) != null) {
883                        int gap = line.indexOf(" ");
884                        if (gap == -1) {
885                            // there must be some issues in the file if no gap can be found
886                                session.getLog().info("Corrupted entry in uid-file " + uidList + " line " + lineNumber++);
887                            continue;
888                        }
889                        
890                        if (uid == Long.valueOf(line.substring(0, line.indexOf(" ")))) {
891                            deletedMessage = newMaildirMessageName(MaildirFolder.this, line.substring(gap + 1, line.length()));
892                            messageCount--;
893                        }
894                        else {
895                            lines.add(line);
896                        }
897                    }
898                    if (deletedMessage != null) {
899                        if (!deletedMessage.getFile().delete())
900                            throw new IOException("Cannot delete file " + deletedMessage.getFile().getAbsolutePath());
901                        writer = new PrintWriter(uidList);
902                        writer.println(createUidListHeader());
903                        for (String entry : lines)
904                            writer.println(entry);
905                    }
906                    return deletedMessage;
907
908                } catch (IOException e) {
909                    throw new MailboxException("Unable to delete msg with uid " + uid, e);
910                } finally {
911                    IOUtils.closeQuietly(writer);
912                    IOUtils.closeQuietly(reader);
913                    IOUtils.closeQuietly(fileReader);
914                }   
915            }
916        }, true);
917        
918
919    }
920    
921    /** 
922     * The absolute path of this folder.
923     */
924    @Override
925    public String toString() {
926        return getRootFile().getAbsolutePath();
927    }
928    
929    public MailboxACL getACL(final MailboxSession session) throws MailboxException {
930        if (acl == null) {
931            acl = readACL(session);
932        }
933        return acl;
934    }
935
936    /**
937     * Read the ACL of the given mailbox from the file system.
938     * 
939     * @param session
940     * @throws MailboxException if there are problems with the aclFile file
941     */
942    private MailboxACL readACL(MailboxSession session) throws MailboxException {
943        // FIXME Do we need this locking?
944        return locker.executeWithLock(session, path, new LockAwareExecution<MailboxACL>() {
945            
946            @Override
947            public MailboxACL execute() throws MailboxException {
948                File f = aclFile;
949                InputStream in = null;
950                Properties props = new Properties();
951                if (f.exists()) {
952                    try {
953                        in = new FileInputStream(f);
954                        props.load(in);
955                    } catch (FileNotFoundException e) {
956                        throw new MailboxException("Unable to read last ACL from "+ f.getAbsolutePath(), e);
957                    } catch (IOException e) {
958                        throw new MailboxException("Unable to read last ACL from "+ f.getAbsolutePath(), e);
959                    }
960                    finally {
961                        IOUtils.closeQuietly(in);
962                    }
963                }
964                
965                return new SimpleMailboxACL(props);
966
967            }
968        }, true);
969        
970    }
971    
972    public void setACL(final MailboxSession session, final MailboxACL acl) throws MailboxException {
973        MailboxACL old = this.acl;
974        if (old != acl && (old == null || !old.equals(acl))) {
975            /* change only if different */
976            saveACL(acl, session);
977            this.acl = acl;
978        }
979        
980    }
981
982    private void saveACL(final MailboxACL acl, final MailboxSession session) throws MailboxException {
983        // FIXME Do we need this locking?
984        locker.executeWithLock(session, path, new LockAwareExecution<Void>() {
985            
986            @Override
987            public Void execute() throws MailboxException {
988                File f = aclFile;
989                OutputStream out = null;
990                Properties props = new Properties();
991                Map<MailboxACLEntryKey, MailboxACLRights> entries = acl.getEntries();
992                if (entries != null) {
993                    for (Entry<MailboxACLEntryKey, MailboxACLRights> en : entries.entrySet()) {
994                        props.put(en.getKey().serialize(), en.getValue().serialize());
995                    }
996                }
997                if (f.exists()) {
998                    try {
999                        out = new FileOutputStream(f);
1000                        props.store(out, "written by "+ getClass().getName());
1001                    } catch (FileNotFoundException e) {
1002                        throw new MailboxException("Unable to read last ACL from "+ f.getAbsolutePath(), e);
1003                    } catch (IOException e) {
1004                        throw new MailboxException("Unable to read last ACL from "+ f.getAbsolutePath(), e);
1005                    }
1006                    finally {
1007                        IOUtils.closeQuietly(out);
1008                    }
1009                }
1010                
1011                return null;
1012
1013            }
1014        }, true);
1015    }
1016
1017    
1018}