001/**************************************************************** 002 * Licensed to the Apache Software Foundation (ASF) under one * 003 * or more contributor license agreements. See the NOTICE file * 004 * distributed with this work for additional information * 005 * regarding copyright ownership. The ASF licenses this file * 006 * to you under the Apache License, Version 2.0 (the * 007 * "License"); you may not use this file except in compliance * 008 * with the License. You may obtain a copy of the License at * 009 * * 010 * http://www.apache.org/licenses/LICENSE-2.0 * 011 * * 012 * Unless required by applicable law or agreed to in writing, * 013 * software distributed under the License is distributed on an * 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 015 * KIND, either express or implied. See the License for the * 016 * specific language governing permissions and limitations * 017 * under the License. * 018 ****************************************************************/ 019package org.apache.james.mailbox.maildir.mail; 020 021import java.io.File; 022import java.io.FileOutputStream; 023import java.io.FilenameFilter; 024import java.io.IOException; 025import java.io.InputStream; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Map; 031import java.util.Map.Entry; 032import java.util.SortedMap; 033 034import javax.mail.Flags; 035import javax.mail.Flags.Flag; 036 037import org.apache.commons.io.FileUtils; 038import org.apache.james.mailbox.MailboxSession; 039import org.apache.james.mailbox.exception.MailboxException; 040import org.apache.james.mailbox.maildir.MaildirFolder; 041import org.apache.james.mailbox.maildir.MaildirMessageName; 042import org.apache.james.mailbox.maildir.MaildirStore; 043import org.apache.james.mailbox.maildir.mail.model.MaildirMessage; 044import org.apache.james.mailbox.model.MessageMetaData; 045import org.apache.james.mailbox.model.MessageRange; 046import org.apache.james.mailbox.model.MessageRange.Type; 047import org.apache.james.mailbox.model.UpdatedFlags; 048import org.apache.james.mailbox.store.SimpleMessageMetaData; 049import org.apache.james.mailbox.store.mail.AbstractMessageMapper; 050import org.apache.james.mailbox.store.mail.model.Mailbox; 051import org.apache.james.mailbox.store.mail.model.Message; 052import org.apache.james.mailbox.store.mail.model.impl.SimpleMessage; 053 054public class MaildirMessageMapper extends AbstractMessageMapper<Integer> { 055 056 private final MaildirStore maildirStore; 057 private final static int BUF_SIZE = 2048; 058 059 public MaildirMessageMapper(MailboxSession session, MaildirStore maildirStore) { 060 super(session, maildirStore, maildirStore); 061 this.maildirStore = maildirStore; 062 } 063 064 /** 065 * @see org.apache.james.mailbox.store.mail.MessageMapper#countMessagesInMailbox(org.apache.james.mailbox.store.mail.model.Mailbox) 066 */ 067 @Override 068 public long countMessagesInMailbox(Mailbox<Integer> mailbox) throws MailboxException { 069 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 070 File newFolder = folder.getNewFolder(); 071 File curFolder = folder.getCurFolder(); 072 File[] newFiles = newFolder.listFiles(); 073 File[] curFiles = curFolder.listFiles(); 074 if (newFiles == null || curFiles == null) 075 throw new MailboxException("Unable to count messages in Mailbox " + mailbox, new IOException( 076 "Not a valid Maildir folder: " + maildirStore.getFolderName(mailbox))); 077 int count = newFiles.length + curFiles.length; 078 return count; 079 } 080 081 /** 082 * @see org.apache.james.mailbox.store.mail.MessageMapper#countUnseenMessagesInMailbox(org.apache.james.mailbox.store.mail.model.Mailbox) 083 */ 084 @Override 085 public long countUnseenMessagesInMailbox(Mailbox<Integer> mailbox) throws MailboxException { 086 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 087 File newFolder = folder.getNewFolder(); 088 File curFolder = folder.getCurFolder(); 089 String[] unseenMessages = curFolder.list(MaildirMessageName.FILTER_UNSEEN_MESSAGES); 090 String[] newUnseenMessages = newFolder.list(MaildirMessageName.FILTER_UNSEEN_MESSAGES); 091 if (newUnseenMessages == null || unseenMessages == null) 092 throw new MailboxException("Unable to count unseen messages in Mailbox " + mailbox, new IOException( 093 "Not a valid Maildir folder: " + maildirStore.getFolderName(mailbox))); 094 int count = newUnseenMessages.length + unseenMessages.length; 095 return count; 096 } 097 098 /** 099 * @see org.apache.james.mailbox.store.mail.MessageMapper#delete(org.apache.james.mailbox.store.mail.model.Mailbox, 100 * org.apache.james.mailbox.store.mail.model.Message) 101 */ 102 @Override 103 public void delete(Mailbox<Integer> mailbox, Message<Integer> message) throws MailboxException { 104 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 105 try { 106 folder.delete(mailboxSession, message.getUid()); 107 } catch (MailboxException e) { 108 throw new MailboxException("Unable to delete Message " + message + " in Mailbox " + mailbox, e); 109 } 110 } 111 112 /** 113 * @see org.apache.james.mailbox.store.mail.MessageMapper#findInMailbox(org.apache.james.mailbox.store.mail.model.Mailbox, 114 * org.apache.james.mailbox.model.MessageRange, 115 * org.apache.james.mailbox.store.mail.MessageMapper.FetchType, int) 116 */ 117 @Override 118 public Iterator<Message<Integer>> findInMailbox(Mailbox<Integer> mailbox, MessageRange set, FetchType fType, int max) 119 throws MailboxException { 120 final List<Message<Integer>> results; 121 final long from = set.getUidFrom(); 122 final long to = set.getUidTo(); 123 final Type type = set.getType(); 124 switch (type) { 125 default: 126 case ALL: 127 results = findMessagesInMailboxBetweenUIDs(mailbox, null, 0, -1, max); 128 break; 129 case FROM: 130 results = findMessagesInMailboxBetweenUIDs(mailbox, null, from, -1, max); 131 break; 132 case ONE: 133 results = findMessageInMailboxWithUID(mailbox, from); 134 break; 135 case RANGE: 136 results = findMessagesInMailboxBetweenUIDs(mailbox, null, from, to, max); 137 break; 138 } 139 return results.iterator(); 140 141 } 142 143 /** 144 * @see org.apache.james.mailbox.store.mail.MessageMapper#findRecentMessageUidsInMailbox(org.apache.james.mailbox.store.mail.model.Mailbox) 145 */ 146 @Override 147 public List<Long> findRecentMessageUidsInMailbox(Mailbox<Integer> mailbox) throws MailboxException { 148 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 149 SortedMap<Long, MaildirMessageName> recentMessageNames = folder.getRecentMessages(mailboxSession); 150 return new ArrayList<Long>(recentMessageNames.keySet()); 151 152 } 153 154 /** 155 * @see org.apache.james.mailbox.store.mail.MessageMapper#findFirstUnseenMessageUid(org.apache.james.mailbox.store.mail.model.Mailbox) 156 */ 157 @Override 158 public Long findFirstUnseenMessageUid(Mailbox<Integer> mailbox) throws MailboxException { 159 List<Message<Integer>> result = findMessagesInMailbox(mailbox, MaildirMessageName.FILTER_UNSEEN_MESSAGES, 1); 160 if (result.isEmpty()) { 161 return null; 162 } else { 163 return result.get(0).getUid(); 164 } 165 } 166 167 /** 168 * @see org.apache.james.mailbox.store.mail.MessageMapper#updateFlags(org.apache.james.mailbox.store.mail.model.Mailbox, 169 * javax.mail.Flags, boolean, boolean, 170 * org.apache.james.mailbox.model.MessageRange) 171 */ 172 @Override 173 public Iterator<UpdatedFlags> updateFlags(final Mailbox<Integer> mailbox, final Flags flags, final boolean value, 174 final boolean replace, final MessageRange set) throws MailboxException { 175 final List<UpdatedFlags> updatedFlags = new ArrayList<UpdatedFlags>(); 176 final MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 177 178 Iterator<Message<Integer>> it = findInMailbox(mailbox, set, FetchType.Metadata, -1); 179 while (it.hasNext()) { 180 final Message<Integer> member = it.next(); 181 Flags originalFlags = member.createFlags(); 182 if (replace) { 183 member.setFlags(flags); 184 } else { 185 Flags current = member.createFlags(); 186 if (value) { 187 current.add(flags); 188 } else { 189 current.remove(flags); 190 } 191 member.setFlags(current); 192 } 193 Flags newFlags = member.createFlags(); 194 195 try { 196 MaildirMessageName messageName = folder.getMessageNameByUid(mailboxSession, member.getUid()); 197 if (messageName != null) { 198 File messageFile = messageName.getFile(); 199 // System.out.println("save existing " + message + 200 // " as " + messageFile.getName()); 201 messageName.setFlags(member.createFlags()); 202 // this automatically moves messages from new to cur if 203 // needed 204 String newMessageName = messageName.getFullName(); 205 206 File newMessageFile; 207 208 // See MAILBOX-57 209 if (newFlags.contains(Flag.RECENT)) { 210 // message is recent so save it in the new folder 211 newMessageFile = new File(folder.getNewFolder(), newMessageName); 212 } else { 213 newMessageFile = new File(folder.getCurFolder(), newMessageName); 214 } 215 long modSeq; 216 // if the flags don't have change we should not try to move 217 // the file 218 if (newMessageFile.equals(messageFile) == false) { 219 FileUtils.moveFile(messageFile, newMessageFile); 220 modSeq = newMessageFile.lastModified(); 221 222 } else { 223 modSeq = messageFile.lastModified(); 224 } 225 member.setModSeq(modSeq); 226 227 updatedFlags.add(new UpdatedFlags(member.getUid(), modSeq, originalFlags, newFlags)); 228 229 long uid = member.getUid(); 230 folder.update(mailboxSession, uid, newMessageName); 231 } 232 } catch (IOException e) { 233 throw new MailboxException("Failure while save Message " + member + " in Mailbox " + mailbox, e); 234 } 235 236 } 237 return updatedFlags.iterator(); 238 239 } 240 241 @Override 242 public Map<Long, MessageMetaData> expungeMarkedForDeletionInMailbox(Mailbox<Integer> mailbox, MessageRange set) 243 throws MailboxException { 244 List<Message<Integer>> results = new ArrayList<Message<Integer>>(); 245 final long from = set.getUidFrom(); 246 final long to = set.getUidTo(); 247 final Type type = set.getType(); 248 switch (type) { 249 default: 250 case ALL: 251 results = findMessagesInMailbox(mailbox, MaildirMessageName.FILTER_DELETED_MESSAGES, -1); 252 break; 253 case FROM: 254 results = findMessagesInMailboxBetweenUIDs(mailbox, MaildirMessageName.FILTER_DELETED_MESSAGES, from, -1, 255 -1); 256 break; 257 case ONE: 258 results = findDeletedMessageInMailboxWithUID(mailbox, from); 259 break; 260 case RANGE: 261 results = findMessagesInMailboxBetweenUIDs(mailbox, MaildirMessageName.FILTER_DELETED_MESSAGES, from, to, 262 -1); 263 break; 264 } 265 Map<Long, MessageMetaData> uids = new HashMap<Long, MessageMetaData>(); 266 for (int i = 0; i < results.size(); i++) { 267 Message<Integer> m = results.get(i); 268 long uid = m.getUid(); 269 uids.put(uid, new SimpleMessageMetaData(m)); 270 delete(mailbox, m); 271 } 272 273 return uids; 274 } 275 276 /** 277 * (non-Javadoc) 278 * 279 * @see org.apache.james.mailbox.store.mail.MessageMapper#move(org.apache.james.mailbox.store.mail.model.Mailbox, 280 * org.apache.james.mailbox.store.mail.model.Message) 281 */ 282 @Override 283 public MessageMetaData move(Mailbox<Integer> mailbox, Message<Integer> original) throws MailboxException { 284 throw new UnsupportedOperationException("Not implemented - see https://issues.apache.org/jira/browse/IMAP-370"); 285 } 286 287 /** 288 * @see org.apache.james.mailbox.store.mail.AbstractMessageMapper#copy(org.apache 289 * .james.mailbox.store.mail.model.Mailbox, long, long, 290 * org.apache.james.mailbox.store.mail.model.Message) 291 */ 292 @Override 293 protected MessageMetaData copy(Mailbox<Integer> mailbox, long uid, long modSeq, Message<Integer> original) 294 throws MailboxException { 295 SimpleMessage<Integer> theCopy = new SimpleMessage<Integer>(mailbox, original); 296 Flags flags = theCopy.createFlags(); 297 flags.add(Flag.RECENT); 298 theCopy.setFlags(flags); 299 return save(mailbox, theCopy); 300 } 301 302 /** 303 * @see org.apache.james.mailbox.store.mail.AbstractMessageMapper#save(org.apache.james.mailbox.store.mail.model.Mailbox, 304 * org.apache.james.mailbox.store.mail.model.Message) 305 */ 306 @Override 307 protected MessageMetaData save(Mailbox<Integer> mailbox, Message<Integer> message) throws MailboxException { 308 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 309 long uid = 0; 310 // a new message 311 // save file to "tmp" folder 312 File tmpFolder = folder.getTmpFolder(); 313 // The only case in which we could get problems with clashing names 314 // is if the system clock 315 // has been set backwards, then the server is restarted with the 316 // same pid, delivers the same 317 // number of messages since its start in the exact same millisecond 318 // as done before and the 319 // random number generator returns the same number. 320 // In order to prevent this case we would need to check ALL files in 321 // all folders and compare 322 // them to this message name. We rather let this happen once in a 323 // billion years... 324 MaildirMessageName messageName = MaildirMessageName.createUniqueName(folder, message.getFullContentOctets()); 325 File messageFile = new File(tmpFolder, messageName.getFullName()); 326 FileOutputStream fos = null; 327 InputStream input = null; 328 try { 329 if (!messageFile.createNewFile()) 330 throw new IOException("Could not create file " + messageFile); 331 fos = new FileOutputStream(messageFile); 332 input = message.getFullContent(); 333 byte[] b = new byte[BUF_SIZE]; 334 int len = 0; 335 while ((len = input.read(b)) != -1) 336 fos.write(b, 0, len); 337 } catch (IOException ioe) { 338 throw new MailboxException("Failure while save Message " + message + " in Mailbox " + mailbox, ioe); 339 } finally { 340 try { 341 if (fos != null) 342 fos.close(); 343 } catch (IOException e) { 344 } 345 try { 346 if (input != null) 347 input.close(); 348 } catch (IOException e) { 349 } 350 } 351 File newMessageFile = null; 352 // delivered via SMTP, goes to ./new without flags 353 if (message.isRecent()) { 354 messageName.setFlags(message.createFlags()); 355 newMessageFile = new File(folder.getNewFolder(), messageName.getFullName()); 356 // System.out.println("save new recent " + message + " as " + 357 // newMessageFile.getName()); 358 } 359 // appended via IMAP (might already have flags etc, goes to ./cur 360 // directly) 361 else { 362 messageName.setFlags(message.createFlags()); 363 newMessageFile = new File(folder.getCurFolder(), messageName.getFullName()); 364 // System.out.println("save new not recent " + message + " as " 365 // + newMessageFile.getName()); 366 } 367 try { 368 FileUtils.moveFile(messageFile, newMessageFile); 369 } catch (IOException e) { 370 // TODO: Try copy and delete 371 throw new MailboxException("Failure while save Message " + message + " in Mailbox " + mailbox, e); 372 } 373 try { 374 uid = folder.appendMessage(mailboxSession, newMessageFile.getName()); 375 message.setUid(uid); 376 message.setModSeq(newMessageFile.lastModified()); 377 return new SimpleMessageMetaData(message); 378 } catch (MailboxException e) { 379 throw new MailboxException("Failure while save Message " + message + " in Mailbox " + mailbox, e); 380 } 381 382 } 383 384 /** 385 * @see org.apache.james.mailbox.store.transaction.TransactionalMapper#endRequest() 386 */ 387 @Override 388 public void endRequest() { 389 // not used 390 391 } 392 393 private List<Message<Integer>> findMessageInMailboxWithUID(Mailbox<Integer> mailbox, long uid) 394 throws MailboxException { 395 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 396 try { 397 MaildirMessageName messageName = folder.getMessageNameByUid(mailboxSession, uid); 398 399 ArrayList<Message<Integer>> messages = new ArrayList<Message<Integer>>(); 400 if (messageName != null && messageName.getFile().exists()) { 401 messages.add(new MaildirMessage(mailbox, uid, messageName)); 402 } 403 return messages; 404 405 } catch (IOException e) { 406 throw new MailboxException("Failure while search for Message with uid " + uid + " in Mailbox " + mailbox, e); 407 } 408 } 409 410 private List<Message<Integer>> findMessagesInMailboxBetweenUIDs(Mailbox<Integer> mailbox, FilenameFilter filter, 411 long from, long to, int max) throws MailboxException { 412 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 413 int cur = 0; 414 SortedMap<Long, MaildirMessageName> uidMap = null; 415 try { 416 if (filter != null) 417 uidMap = folder.getUidMap(mailboxSession, filter, from, to); 418 else 419 uidMap = folder.getUidMap(mailboxSession, from, to); 420 421 ArrayList<Message<Integer>> messages = new ArrayList<Message<Integer>>(); 422 for (Entry<Long, MaildirMessageName> entry : uidMap.entrySet()) { 423 messages.add(new MaildirMessage(mailbox, entry.getKey(), entry.getValue())); 424 if (max != -1) { 425 cur++; 426 if (cur >= max) 427 break; 428 } 429 } 430 return messages; 431 } catch (IOException e) { 432 throw new MailboxException("Failure while search for Messages in Mailbox " + mailbox, e); 433 } 434 435 } 436 437 private List<Message<Integer>> findMessagesInMailbox(Mailbox<Integer> mailbox, FilenameFilter filter, int limit) 438 throws MailboxException { 439 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 440 try { 441 SortedMap<Long, MaildirMessageName> uidMap = folder.getUidMap(mailboxSession, filter, limit); 442 443 ArrayList<Message<Integer>> filtered = new ArrayList<Message<Integer>>(uidMap.size()); 444 for (Entry<Long, MaildirMessageName> entry : uidMap.entrySet()) 445 filtered.add(new MaildirMessage(mailbox, entry.getKey(), entry.getValue())); 446 return filtered; 447 } catch (IOException e) { 448 throw new MailboxException("Failure while search for Messages in Mailbox " + mailbox, e); 449 } 450 451 } 452 453 private List<Message<Integer>> findDeletedMessageInMailboxWithUID(Mailbox<Integer> mailbox, long uid) 454 throws MailboxException { 455 MaildirFolder folder = maildirStore.createMaildirFolder(mailbox); 456 try { 457 MaildirMessageName messageName = folder.getMessageNameByUid(mailboxSession, uid); 458 ArrayList<Message<Integer>> messages = new ArrayList<Message<Integer>>(); 459 if (MaildirMessageName.FILTER_DELETED_MESSAGES.accept(null, messageName.getFullName())) { 460 messages.add(new MaildirMessage(mailbox, uid, messageName)); 461 } 462 return messages; 463 464 } catch (IOException e) { 465 throw new MailboxException("Failure while search for Messages in Mailbox " + mailbox, e); 466 } 467 468 } 469 470 /** 471 * @see org.apache.james.mailbox.store.transaction.TransactionalMapper#begin() 472 */ 473 @Override 474 protected void begin() throws MailboxException { 475 // nothing todo 476 } 477 478 /** 479 * @see org.apache.james.mailbox.store.transaction.TransactionalMapper#commit() 480 */ 481 @Override 482 protected void commit() throws MailboxException { 483 // nothing todo 484 } 485 486 /** 487 * @see org.apache.james.mailbox.store.transaction.TransactionalMapper#rollback() 488 */ 489 @Override 490 protected void rollback() throws MailboxException { 491 // nothing todo 492 } 493 494}