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}