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}