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.File; 022import java.io.FileNotFoundException; 023import java.io.FilenameFilter; 024import java.lang.management.ManagementFactory; 025import java.net.InetAddress; 026import java.net.UnknownHostException; 027import java.util.Date; 028import java.util.Random; 029import java.util.concurrent.atomic.AtomicInteger; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import javax.mail.Flags; 034 035public class MaildirMessageName { 036 037 // the flags in Maildir message names 038 public static final String FLAG_DRAFT = "D"; 039 public static final String FLAG_FLAGGED = "F"; 040 public static final String FLAG_ANSWERD = "R"; 041 public static final String FLAG_SEEN = "S"; 042 public static final String FLAG_DELETED = "T"; 043 044 // patterns 045 public static final String PATTERN_STRING_MESSAGE_NAME = "\\d+\\.\\w+\\..+?"; 046 public static final String PATTERN_STRING_FLAGS = ":2,[DFRST]*"; 047 public static final String PATTERN_STRING_SIZE = ",S=\\d+"; 048 public static final Pattern PATTERN_MESSAGE = 049 Pattern.compile(PATTERN_STRING_MESSAGE_NAME + optional(PATTERN_STRING_SIZE) + optional(PATTERN_STRING_FLAGS)); 050 051 public static final Pattern PATTERN_UNSEEN_MESSAGES = 052 Pattern.compile(PATTERN_STRING_MESSAGE_NAME + PATTERN_STRING_SIZE + optional(":2,[^S]*")); 053 054 public static final FilenameFilter FILTER_UNSEEN_MESSAGES = createRegexFilter(PATTERN_UNSEEN_MESSAGES); 055 056 public static final Pattern PATTERN_DELETED_MESSAGES = 057 Pattern.compile(PATTERN_STRING_MESSAGE_NAME + PATTERN_STRING_SIZE + ":2,.*" + FLAG_DELETED); 058 059 public static final FilenameFilter FILTER_DELETED_MESSAGES = createRegexFilter(PATTERN_DELETED_MESSAGES); 060 061 /** 062 * The number of deliveries done by the server since its last start 063 */ 064 private static AtomicInteger deliveries = new AtomicInteger(0); 065 066 /** 067 * A random generator for the random part in the unique message names 068 */ 069 private static Random random = new Random(); 070 071 /** 072 * The process id of the server process 073 */ 074 private static String processName = ManagementFactory.getRuntimeMXBean().getName(); 075 static { 076 String[] parts = processName.split("@"); 077 if (parts.length > 1) 078 processName = parts[0]; 079 } 080 081 /** 082 * The host name of the machine the server is running on 083 */ 084 private static String currentHostname; 085 static { 086 try { 087 currentHostname = InetAddress.getLocalHost().getHostName(); 088 } catch (UnknownHostException e) { 089 currentHostname = "localhost"; 090 } 091 } 092 093 private String fullName; 094 private File file; 095 private MaildirFolder parentFolder; 096 private String timestamp; 097 private String uniqueString; 098 private String hostnameAndMeta; // tim-erwin.de,S=1000:2,RS 099 private String hostname; 100 private String sizeString; 101 private String flagsString; 102 private boolean isSplit; 103 private Date internalDate; 104 private Long size; 105 private Flags flags; 106 private boolean messageNameStrictParse = false; 107 108 public MaildirMessageName(MaildirFolder parentFolder, String fullName) { 109 this.parentFolder = parentFolder; 110 setFullName(fullName); 111 } 112 113 public boolean isMessageNameStrictParse() { 114 return messageNameStrictParse; 115 } 116 117 public void setMessageNameStrictParse(boolean messageNameStrictParse) { 118 this.messageNameStrictParse = messageNameStrictParse; 119 } 120 121 /** 122 * Tests whether the file or directory belonging to this {@link MaildirFolder} exists. 123 * If the file exists, its absolute path is written to absPath. 124 * TODO: If the flags have changed or the file doesn't exist any more, the uidlist should be updated 125 * @return true if the file or directory belonging to this {@link MaildirFolder} exists ; false otherwise 126 */ 127 public boolean exists() { 128 if (file != null && file.isFile()) 129 return true; 130 File assumedFile1 = new File(parentFolder.getCurFolder(), fullName); 131 if (assumedFile1.isFile()) { 132 file = assumedFile1; 133 return true; 134 } 135 File assumedFile2 = new File(parentFolder.getNewFolder(), fullName); 136 if (assumedFile2.isFile()) { 137 file = assumedFile2; 138 return true; 139 } 140 // check if maybe the flags have changed which means 141 // list the files in the cur and new folder and check if the message is there 142 FilenameFilter filter = getFilenameFilter(); 143 File[] matchingFiles1 = parentFolder.getCurFolder().listFiles(filter); 144 if (matchingFiles1.length == 1) { 145 setFullName(matchingFiles1[0].getName()); 146 file = matchingFiles1[0]; 147 return true; 148 } 149 File[] matchingFiles2 = parentFolder.getNewFolder().listFiles(filter); 150 if (matchingFiles2.length == 1) { 151 setFullName(matchingFiles2[0].getName()); 152 file = matchingFiles2[0]; 153 return true; 154 } 155 return false; 156 } 157 158 /** 159 * Sets the fullName of this {@link MaildirMessageName} if different from the current one. 160 * As this invalidates the parsed elements, they are being reset. 161 * @param fullName A name of a message file in the correct Maildir format 162 */ 163 public void setFullName(String fullName) { 164 if (this.fullName == null || !this.fullName.equals(fullName)) { 165 this.fullName = fullName; 166 this.file = null; 167 this.isSplit = false; 168 this.internalDate = null; 169 this.size = null; 170 this.flags = null; 171 } 172 } 173 174 /** 175 * Returns the full name of this message including size and flags if available. 176 * @return the full name of this message 177 */ 178 public String getFullName() { 179 if (this.fullName == null) { 180 StringBuilder fullBuffer = new StringBuilder(); 181 fullBuffer.append(timestamp); 182 fullBuffer.append("."); 183 fullBuffer.append(uniqueString); 184 fullBuffer.append("."); 185 fullBuffer.append(hostname); 186 if (sizeString != null) 187 fullBuffer.append(sizeString); 188 if (flagsString != null) 189 fullBuffer.append(flagsString); 190 fullName = fullBuffer.toString(); 191 } 192 return fullName; 193 } 194 195 /** 196 * Returns a {@link File} object of the message denoted by this {@link MaildirMessageName}. 197 * Also checks for different flags if it cannot be found directly. 198 * @return A {@link File} object 199 * @throws FileNotFoundException If there is no file for the given name 200 */ 201 public File getFile() throws FileNotFoundException { 202 if (exists()) 203 return file; 204 else 205 throw new FileNotFoundException("There is no file for message name " + fullName 206 + " in mailbox " + parentFolder.getRootFile().getAbsolutePath()); 207 } 208 209 /** 210 * Creates a filter which matches the message file even if the flags have changed 211 * @return filter for this message 212 */ 213 public FilenameFilter getFilenameFilter() { 214 split(); 215 StringBuilder pattern = new StringBuilder(); 216 pattern.append(timestamp); 217 pattern.append("\\."); 218 pattern.append(uniqueString); 219 pattern.append("\\."); 220 pattern.append(hostname); 221 pattern.append(".*"); 222 return createRegexFilter(Pattern.compile(pattern.toString())); 223 } 224 225 /** 226 * Splits up the full file name if necessary. 227 */ 228 private void split() { 229 if (!isSplit) { 230 splitFullName(); 231 splitHostNameAndMeta(); 232 isSplit = true; 233 } 234 } 235 236 /** 237 * Splits up the full file name into its main components timestamp, 238 * uniqueString and hostNameAndMeta and fills the respective variables. 239 */ 240 private void splitFullName() { 241 int firstEnd = fullName.indexOf('.'); 242 int secondEnd = fullName.indexOf('.', firstEnd + 1); 243 timestamp = fullName.substring(0, firstEnd); 244 uniqueString = fullName.substring(firstEnd + 1, secondEnd); 245 hostnameAndMeta = fullName.substring(secondEnd + 1, fullName.length()); 246 } 247 248 /** 249 * Splits up the third part of the file name (e.g. tim-erwin.de,S=1000:2,RS) 250 * into its components hostname, size and flags and fills the respective variables. 251 */ 252 private void splitHostNameAndMeta() { 253 String[] hostnamemetaFlags = hostnameAndMeta.split(":", 2); 254 if (hostnamemetaFlags.length >= 1) { 255 this.hostnameAndMeta = hostnamemetaFlags[0]; 256 int firstEnd = hostnameAndMeta.indexOf(','); 257 258 // read size field if existent 259 if (firstEnd > 0) { 260 hostname = hostnameAndMeta.substring(0, firstEnd); 261 String attrStr = hostnameAndMeta.substring(firstEnd); 262 String[] fields = attrStr.split(","); 263 for (String field : fields) { 264 if (field.startsWith("S=")) { 265 sizeString = "," + field; 266 } 267 } 268 } else { 269 sizeString = null; 270 hostname = this.hostnameAndMeta; 271 } 272 } 273 274 if (hostnamemetaFlags.length >= 2) { 275 this.flagsString = ":" + hostnamemetaFlags[1]; 276 } 277 if (isMessageNameStrictParse()) { 278 if (sizeString == null) { 279 throw new IllegalArgumentException("No message size found in message name: "+ fullName); 280 } 281 if (flagsString == null) { 282 throw new IllegalArgumentException("No flags found in message name: "+ fullName); 283 } 284 } 285 } 286 287 /** 288 * Sets new flags for this message name. 289 * @param flags 290 */ 291 public void setFlags(Flags flags) { 292 if (this.flags != flags) { 293 split(); // save all parts 294 this.flags = flags; 295 this.flagsString = encodeFlags(flags); 296 this.fullName = null; // invalidate the fullName 297 } 298 } 299 300 /** 301 * Decodes the flags part of the file name if necessary and returns the appropriate Flags object. 302 * @return The {@link Flags} of this message 303 */ 304 public Flags getFlags() { 305 if (flags == null) { 306 split(); 307 if (flagsString == null) 308 return null; 309 if (flagsString.length() >= 3) 310 flags = decodeFlags(flagsString.substring(3)); // skip the ":2," part 311 } 312 return flags; 313 } 314 315 /** 316 * Decodes the size part of the file name if necessary and returns the appropriate Long. 317 * @return The size of this message as a {@link Long} 318 */ 319 public Long getSize() { 320 if (size == null) { 321 split(); 322 if (sizeString == null) 323 return null; 324 if (!sizeString.startsWith(",S=")) 325 return null; 326 size = Long.valueOf(sizeString.substring(3)); // skip the ",S=" part 327 } 328 return size; 329 } 330 331 /** 332 * Decodes the time part of the file name if necessary and returns the appropriate Date. 333 * @return The time of this message as a {@link Date} 334 */ 335 public Date getInternalDate() { 336 if (internalDate == null) { 337 split(); 338 if (timestamp == null) 339 return null; 340 internalDate = new Date(Long.valueOf(timestamp) * 1000); 341 } 342 return internalDate; 343 } 344 345 /** 346 * Composes the base name consisting of timestamp, unique string and host name 347 * witout the size and flags. 348 * @return The base name 349 */ 350 public String getBaseName() { 351 split(); 352 StringBuilder baseName = new StringBuilder(); 353 baseName.append(timestamp); 354 baseName.append("."); 355 baseName.append(uniqueString); 356 baseName.append("."); 357 baseName.append(hostname); 358 return baseName.toString(); 359 } 360 361 /** 362 * Creates a String that represents the provided Flags 363 * @param flags The flags to encode 364 * @return A String valid for Maildir 365 */ 366 public String encodeFlags(Flags flags) { 367 StringBuilder localFlagsString = new StringBuilder(":2,"); 368 if (flags.contains(Flags.Flag.DRAFT)) 369 localFlagsString.append(FLAG_DRAFT); 370 if (flags.contains(Flags.Flag.FLAGGED)) 371 localFlagsString.append(FLAG_FLAGGED); 372 if (flags.contains(Flags.Flag.ANSWERED)) 373 localFlagsString.append(FLAG_ANSWERD); 374 if (flags.contains(Flags.Flag.SEEN)) 375 localFlagsString.append(FLAG_SEEN); 376 if (flags.contains(Flags.Flag.DELETED)) 377 localFlagsString.append(FLAG_DELETED); 378 return localFlagsString.toString(); 379 } 380 381 /** 382 * Takes a String which is "Maildir encoded" and translates it 383 * into a {@link Flags} object. 384 * @param flagsString The String with the flags 385 * @return A Flags object containing the flags read form the String 386 */ 387 public Flags decodeFlags(String flagsString) { 388 Flags localFlags = new Flags(); 389 if (flagsString.contains(FLAG_DRAFT)) 390 localFlags.add(Flags.Flag.DRAFT); 391 if (flagsString.contains(FLAG_FLAGGED)) 392 localFlags.add(Flags.Flag.FLAGGED); 393 if (flagsString.contains(FLAG_ANSWERD)) 394 localFlags.add(Flags.Flag.ANSWERED); 395 if (flagsString.contains(FLAG_SEEN)) 396 localFlags.add(Flags.Flag.SEEN); 397 if (flagsString.contains(FLAG_DELETED)) 398 localFlags.add(Flags.Flag.DELETED); 399 return localFlags; 400 } 401 402 /** 403 * Returns and increases a global counter for the number 404 * of deliveries done since the server has been started. 405 * This is used for the creation of message names. 406 * @return The number of (attempted) deliveries until now 407 */ 408 static private long getNextDeliveryNumber() { 409 return deliveries.getAndIncrement(); 410 } 411 412 /** 413 * Create a name for a message according to <a href="http://cr.yp.to/proto/maildir.html" /><br/> 414 * The following elements are used: 415 * <br><br/> 416 * "A unique name has three pieces, separated by dots. On the left is the result of time() 417 * or the second counter from gettimeofday(). On the right is the result of gethostname(). 418 * (To deal with invalid host names, replace / with \057 and : with \072.) 419 * In the middle is a delivery identifier, discussed below. 420 * <br/><br/> 421 * Modern delivery identifiers are created by concatenating enough of the following strings 422 * to guarantee uniqueness: 423 * <br/><br/> 424 * [...]<br/> 425 * * Rn, where n is (in hexadecimal) the output of the operating system's unix_cryptorandomnumber() system call, or an equivalent source such as /dev/urandom. Unfortunately, some operating systems don't include cryptographic random number generators.<br/> 426 * [...]<br/> 427 * * Mn, where n is (in decimal) the microsecond counter from the same gettimeofday() used for the left part of the unique name.<br/> 428 * * Pn, where n is (in decimal) the process ID.<br/> 429 * * Qn, where n is (in decimal) the number of deliveries made by this process.<br/> 430 * <br/> 431 * [...]" 432 * 433 * @return unique message name 434 */ 435 public static MaildirMessageName createUniqueName(MaildirFolder parentFolder, long size) { 436 String timestamp = String.valueOf(System.currentTimeMillis()); 437 timestamp = timestamp.substring(0, timestamp.length()-3); // time in seconds 438 StringBuilder uniquePart = new StringBuilder(); 439 uniquePart.append(Integer.toHexString(random.nextInt())); // random number as hex 440 uniquePart.append(timestamp.substring(timestamp.length()-3)); // milliseconds 441 uniquePart.append(processName); // process name 442 uniquePart.append(getNextDeliveryNumber()); // delivery number 443 String sizeString = ",S=" + String.valueOf(size); 444 String fullName = timestamp + "." + uniquePart.toString() + "." + currentHostname + sizeString; 445 MaildirMessageName uniqueName = new MaildirMessageName(parentFolder, fullName); 446 uniqueName.timestamp = timestamp; 447 uniqueName.uniqueString = uniquePart.toString(); 448 uniqueName.hostname = currentHostname; 449 uniqueName.sizeString = sizeString; 450 uniqueName.isSplit = true; 451 uniqueName.size = size; 452 return uniqueName; 453 } 454 455 public static FilenameFilter createRegexFilter(final Pattern pattern) { 456 return new FilenameFilter() { 457 @Override 458 public boolean accept(File dir, String name) { 459 Matcher matcher = pattern.matcher(name); 460 return matcher.matches(); 461 } 462 }; 463 } 464 465 public static String optional(String pattern) { 466 return "(" + pattern + ")?"; 467 } 468 469 @Override 470 public String toString() { 471 return getFullName(); 472 } 473}