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}