1 /* 2 * Copyright (c) 2005-2007 Creative Sphere Limited. 3 * All rights reserved. This program and the accompanying materials 4 * are made available under the terms of the Eclipse Public License v1.0 5 * which accompanies this distribution, and is available at 6 * http://www.eclipse.org/legal/epl-v10.html 7 * 8 * Contributors: 9 * 10 * Creative Sphere - initial API and implementation 11 * 12 */ 13 package org.abstracthorizon.mercury.maildir; 14 15 import java.io.File; 16 import java.lang.ref.Reference; 17 import java.lang.ref.WeakReference; 18 import java.util.Map; 19 import java.util.WeakHashMap; 20 21 import javax.activation.CommandMap; 22 import javax.activation.MailcapCommandMap; 23 import javax.mail.Folder; 24 import javax.mail.MessagingException; 25 import javax.mail.Session; 26 import javax.mail.Store; 27 import javax.mail.URLName; 28 29 30 /** 31 * <p>This is simple maildir implementation store. It reads files from the <i>cur</i> and 32 * <i>new</i> subdirectories of the store or subdirectories of store's folders. When new 33 * message is created <i>tmp</i> subdirectory is used for RFC-822 mail to be written 34 * in it and then it is renamed to <i>new</i> directory. This implementation doesn't 35 * write/read to/from any special index files and it is not implementing <code>UIDFolder</code> 36 * interface from <code>javax.mail</code> API. Messages are ordered in folders in the 37 * way <code>java.io.File.list</code> method is returning files.</p> 38 * 39 * <p>It takes a path of the store an input parameter (path in URLName). Also 40 * it uses following session properties: 41 * 42 * <table border="1"> 43 * <tr><th>property</th><th>default value if not present</th><th>description</th></tr> 44 * <tr> 45 * <td><code>maildir.leadingDot</code></td> 46 * <td><code>true</code></td> 47 * <td>If this property has value of <code>true</code> then folders' directories are 48 * with the leading dot. This is standard used by most implementations of maildir. If value 49 * is set to <code>false</code> then folders' directories are stored without leading dot. 50 * Note: If directories are without leading dot then <i>tmp</i>, <i>cur</i> and <i>new</i> 51 * are invalid names of folders. 52 * </td> 53 * </tr> 54 * <tr> 55 * <td><code>maildir.infoSeparator</code></td> 56 * <td><code>System.getProperty("path.separator")</code> is ":" then 57 * ":". Othewise "."</td> 58 * <td>If value is set then it will be used as separator of info part in maildir's filename. 59 * </tr> 60 * <tr> 61 * <td><code>maildir.httpSyntax</code></td> 62 * <td><code>false</code> is ":" then 63 * ":". Othewise "."</td> 64 * <td>If set to <code>true</code> then URLName's syntax for maildir should be: 65 * <code>maildir://user:password@host:port/foldername?base=base_directory_of_maildir_store</code>. 66 * Otherwise it is <code>maildir://user:password@host:port/base_directory_of_maildir_store#foldername</code>. 67 * Note: foldername is not used when store is being obtained. 68 * </td> 69 * </tr> 70 * <tr> 71 * <td><code>maildir.home</code></td> 72 * <td> </td> 73 * <td>If maildir store's base directory is not set in URL this session property will be queried and its 74 * value used if present.</td> 75 * </tr> 76 * </table> 77 * </p> 78 * <p>Maildir store's base directory is searched in following order: 79 * <ol> 80 * <li>URLName (check <code>maildir.httpSyntax</code> for more details). If not present then</li> 81 * <li><code>maildir.home</code> from session. If not present then</li> 82 * <li><code>user.home</code> from system properties with <code>.mail</code> appended to the path. If that directory is not present then</li> 83 * <li><code>user.home</code> from system properties with <code>Mail</code> appended to the path.</li> 84 * </ol> 85 * 86 * </p> 87 * <p>If all fails it obtaining the store will fail as well.</p> 88 * <p>This implementation also does substitutions in supplied path based on url name parameters: 89 * <ul> 90 * <li><code>{user}</code> in the path is substituted by username from url name</li> 91 * <li><code>{protocol}</code> in the path is substituted by protocol from url name</li> 92 * <li><code>{port}</code> in the path is substituted by port from url name</li> 93 * <li><code>{host}</code> in the path is substituted by host from url name</li> 94 * </ul> 95 * </p> 96 * 97 * <p>This class implements centralised cached directory of all currently open folder. 98 * </p> 99 * 100 * <p>Folder data actually contains messages while folders have only wrappers 101 * with references to folder data's messages. Each folder uses wrappers to maintain 102 * message number. All open folders are registered with folder data and changes 103 * one folder, through folder data, are immediately propagated to other folder 104 * over the same folder data (same virtually folder). 105 * Folder data here serves as central repository of messages over one directory. 106 * </p> 107 * 108 * <p>If new messages are discovered in directory or one folder over folder data 109 * has new messages added by append method, all other folders over the same folder 110 * data will be notified of new messages and then will immediately appear in their 111 * lists</p> 112 * 113 * <p>Similarily if one folder expunges messages or messages are detected as deleted 114 * from the underlaying directory, all folder's will be notified of the change and 115 * all messages will be marked as expunged.</p> 116 * 117 118 * @author Daniel Senudula 119 */ 120 public class MaildirStore extends Store { 121 122 /** Leading dot session attribute name */ 123 public static final String LEADING_DOT = "maildir.leadingDot"; 124 125 /** Store's home directory session attribute name */ 126 public static final String HOME = "maildir.home"; 127 128 /** Info separator session attribute name */ 129 public static final String INFO_SEPARATOR = "maildir.infoSeparator"; 130 131 /** Http syntax session attribute name */ 132 public static final String HTTP_SYNTAX = "maildir.httpSyntax"; 133 134 /** Amount of time folder is going to be kept in list of folders */ 135 public static final long MAX_FOLDER_DATA_LIFE = 1000*60*60; // 1 hour 136 137 /** Store's base directory file */ 138 protected File base; 139 140 /** Cached leading dot property */ 141 protected boolean leadingDot = true; 142 143 /** Cached http syntax property */ 144 protected boolean httpSyntax = false; 145 146 /** Cached info separator property */ 147 protected char infoSeparator; 148 149 static { 150 CommandMap commandMap = CommandMap.getDefaultCommandMap(); 151 if (commandMap instanceof MailcapCommandMap) { 152 MailcapCommandMap mailcapCommandMap = (MailcapCommandMap)commandMap; 153 mailcapCommandMap.addMailcap("multipart/*;; x-java-content-handler="+MaildirMimeMultipartDataContentHandler.class.getName()); 154 } 155 } 156 157 /** Cache */ 158 protected Map<File, Reference<MaildirFolderData>> directories = new WeakHashMap<File, Reference<MaildirFolderData>>(); 159 160 161 /** 162 * Constructor 163 * @param session mail session 164 * @param urlname url name 165 */ 166 public MaildirStore(Session session, URLName urlname) { 167 super(session, urlname); 168 169 String leadingDotString = session.getProperty(LEADING_DOT); 170 if (leadingDotString != null) { 171 leadingDot = "true".equals(leadingDotString); 172 } 173 174 String httpSyntaxString = session.getProperty(HTTP_SYNTAX); 175 if (httpSyntaxString != null) { 176 httpSyntax = "true".equalsIgnoreCase(httpSyntaxString); 177 } 178 179 String infoSeparatorString = session.getProperty(INFO_SEPARATOR); 180 if ((infoSeparatorString != null) && (infoSeparatorString.length() > 0)) { 181 infoSeparator = infoSeparatorString.charAt(0); 182 } else { 183 if (":".equals(System.getProperty("path.separator"))) { 184 infoSeparator = ':'; 185 } else { 186 infoSeparator = '.'; 187 } 188 } 189 190 if (httpSyntax) { 191 parseURLName(urlname); 192 } else { 193 String baseFileName = urlname.getFile(); 194 base = createBaseFile(urlname, baseFileName); 195 } 196 197 if (base == null) { 198 String homeString = session.getProperty(HOME); 199 if (homeString != null) { 200 base = new File(homeString); 201 } else { 202 203 File home = new File(System.getProperty("user.home")); 204 base = new File(home, ".mail"); 205 if (!base.exists()) { 206 File t = base; 207 base = new File(home, "Mail"); 208 if (base.exists()) { 209 if (!"true".equals(session.getProperty(LEADING_DOT))) { 210 leadingDot = false; 211 } 212 } else { 213 base = t; 214 } 215 } 216 } 217 } 218 } 219 220 /** 221 * Parses url name 222 * @param urlname url name 223 */ 224 protected void parseURLName(URLName urlname) { 225 String file = urlname.getFile(); 226 int i = file.indexOf('?'); 227 if (i >= 0) { 228 String params = file.substring(i+1); 229 i = 0; 230 int j = params.indexOf(',', i); 231 while (j > 0) { 232 processParam(urlname, params.substring(i, j)); 233 i = j+1; 234 j = params.indexOf(',', i); 235 } 236 processParam(urlname, params.substring(i)); 237 } 238 } 239 240 /** 241 * Processes singe parameter from url name 242 * @param urlName url name 243 * @param param parameter 244 */ 245 protected void processParam(URLName urlName, String param) { 246 if (param.startsWith("base=")) { 247 String baseString = param.substring(5); 248 base = createBaseFile(urlName, baseString); 249 } 250 } 251 252 /** 253 * Creates base file and substitues <code>{user}</code>, <code>{port}</code>, 254 * <code>{host}</code> and <code>{protocol}</code> 255 * @param urlName url name 256 * @param baseName directory path 257 * @return created file that represents base directory of the store 258 */ 259 protected File createBaseFile(URLName urlName, String baseName) { 260 baseName = replace(baseName, "{protocol}", urlName.getProtocol()); 261 baseName = replace(baseName, "{host}", urlName.getHost()); 262 baseName = replace(baseName, "{port}", Integer.toString(urlName.getPort())); 263 baseName = replace(baseName, "{user}", urlName.getUsername()); 264 265 return new File(baseName); 266 } 267 268 /** 269 * Replaces substring 270 * @param s string on which operation is done 271 * @param what what to be replaced 272 * @param with new value to be placed instead 273 * @return new string 274 */ 275 protected String replace(String s, String what, String with) { 276 int i = s.indexOf(what); 277 if (i > 0) { 278 return s.substring(0, i)+with+s.substring(i+what.length()); 279 } else { 280 return s; 281 } 282 } 283 284 /** 285 * Returns info separator 286 * @return info separator 287 */ 288 public char getInfoSeparator() { 289 return infoSeparator; 290 } 291 292 /** 293 * Returns leading dot 294 * @return leading dot 295 */ 296 public boolean isLeadingDot() { 297 return leadingDot; 298 } 299 300 /** 301 * Returns http syntax 302 * @return http syntax 303 */ 304 public boolean isHttpSyntax() { 305 return httpSyntax; 306 } 307 308 /** 309 * Retuns base file (store's base directory) 310 * @return base file 311 */ 312 public File getBaseFile() { 313 return base; 314 } 315 316 /** 317 * Returns default folder 318 * @return default folder 319 * @throws MessagingException 320 */ 321 public Folder getDefaultFolder() throws MessagingException { 322 return getFolder(""); 323 } 324 325 /** 326 * Returns folder data for given folder. This is needed for new folder is created. 327 * This implementation obtains proper folder's directory and passes file to 328 * {@link #getFolderData(File)} method. 329 * @param name full folder's name 330 * @return folder data 331 */ 332 protected MaildirFolderData getFolderData(String name) { 333 if (!isConnected()) { 334 throw new IllegalStateException("Store is not connected"); 335 } 336 name = name.replace('/', '.'); 337 name = name.replace('\\', '.'); 338 if (name.startsWith(".")) { 339 name = name.substring(1); 340 } 341 if ("inbox".equalsIgnoreCase(name)) { 342 name = "inbox"; 343 } 344 if ((name.length() > 0) && leadingDot) { 345 name = '.'+name; 346 } 347 File file = new File(base, name); 348 return getFolderData(file); 349 } 350 351 /** 352 * This method returns folder data needed for folder to operate on. 353 * If first checks cache and if there is no folder data in it 354 * new will be created and stored in the cache. 355 * @param file directory 356 * @return new folder data 357 */ 358 protected MaildirFolderData getFolderData(File file) { 359 Reference<MaildirFolderData> ref = directories.get(file); 360 MaildirFolderData folderData = null; 361 if (ref != null) { 362 folderData = (MaildirFolderData)ref.get(); 363 if ((System.currentTimeMillis() - folderData.getLastAccessed()) > MAX_FOLDER_DATA_LIFE) { 364 folderData = null; 365 directories.remove(file); 366 } 367 } 368 if (folderData == null) { 369 folderData = createFolderData(file); 370 directories.put(file, new WeakReference<MaildirFolderData>(folderData)); 371 } 372 373 return folderData; 374 } 375 376 /** 377 * This implementation creates {@link MaildirFolderData} from supplied file. 378 * @param file file 379 * @return new maildir folder data 380 */ 381 protected MaildirFolderData createFolderData(File file) { 382 return new MaildirFolderData(this, file); 383 } 384 385 /** 386 * Returns folder with given folder data 387 * @param folderData folder data 388 * @return new folder instance 389 * @throws MessagingException 390 */ 391 public Folder getParentFolder(MaildirFolderData folderData) throws MessagingException { 392 String parentFolderName = folderData.getParentFolderName(); 393 return getFolder(parentFolderName); 394 } 395 396 /** 397 * Returns new folder from full folder's name 398 * @param name full folder's name 399 * @return new folder 400 * @throws MessagingException 401 */ 402 public Folder getFolder(String name) throws MessagingException { 403 if (!isConnected()) { 404 throw new IllegalStateException("Store is not connected"); 405 } 406 MaildirFolderData folderData = getFolderData(name); 407 return createFolder(folderData); 408 } 409 410 /** 411 * Returns new folder from URLName. if <code>maildir.httpSyntax</code> attribute 412 * has value of <code>true</code> then url name's file is used, otherwise 413 * url name's ref is used. 414 * @param urlName url name 415 * @return new folder 416 * @throws MessagingException 417 */ 418 public Folder getFolder(URLName urlName) throws MessagingException { 419 if (isHttpSyntax()) { 420 return getFolder(urlName.getFile()); 421 } else { 422 return getFolder(urlName.getRef()); 423 } 424 } 425 426 /** 427 * Creates new folder instance with given folder data. This metod is to be overriden by 428 * class extensions. 429 * @param folderData folder data 430 * @return new folder instance 431 */ 432 protected MaildirFolder createFolder(MaildirFolderData folderData) { 433 return new MaildirFolder(this, folderData); 434 } 435 436 /** 437 * This method returns <code>true</code> always. 438 * @param host ignored 439 * @param port ignored 440 * @param user ignored 441 * @param password ignored 442 * @return <code>true</code> 443 * @throws MessagingException never 444 */ 445 protected boolean protocolConnect(String host, 446 int port, 447 String user, 448 String password) 449 throws MessagingException { 450 return true; 451 } 452 }