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.model;
020
021import org.apache.commons.io.IOUtils;
022import org.apache.james.mailbox.maildir.MaildirFolder;
023import org.apache.james.mailbox.maildir.MaildirMessageName;
024import org.apache.james.mailbox.store.mail.model.AbstractMessage;
025import org.apache.james.mailbox.store.mail.model.Mailbox;
026import org.apache.james.mailbox.store.mail.model.Property;
027import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
028import org.apache.james.mailbox.store.streaming.CountingInputStream;
029import org.apache.james.mailbox.store.streaming.LimitingFileInputStream;
030import org.apache.james.mime4j.MimeException;
031import org.apache.james.mime4j.message.DefaultBodyDescriptorBuilder;
032import org.apache.james.mime4j.message.MaximalBodyDescriptor;
033import org.apache.james.mime4j.stream.EntityState;
034import org.apache.james.mime4j.stream.MimeConfig;
035import org.apache.james.mime4j.stream.MimeTokenStream;
036import org.apache.james.mime4j.stream.RecursionMode;
037
038import javax.mail.Flags;
039import javax.mail.util.SharedFileInputStream;
040import java.io.*;
041import java.util.Date;
042import java.util.List;
043
044public class MaildirMessage extends AbstractMessage<Integer> {
045
046    private MaildirMessageName messageName;
047    private int bodyStartOctet;
048    private final PropertyBuilder propertyBuilder = new PropertyBuilder();
049    private boolean parsed;
050    private boolean answered;
051    private boolean deleted;
052    private boolean draft;
053    private boolean flagged;
054    private boolean recent;
055    private boolean seen;
056    private Mailbox<Integer> mailbox;
057    private long uid;
058    protected boolean newMessage;
059    private long modSeq;
060    
061    public MaildirMessage(Mailbox<Integer> mailbox, long uid, MaildirMessageName messageName) throws IOException {
062        this.mailbox = mailbox;
063        setUid(uid);
064        setModSeq(messageName.getFile().lastModified());
065        Flags flags = messageName.getFlags();
066        
067        // Set the flags for the message and respect if its RECENT
068        // See MAILBOX-84
069        File file = messageName.getFile();
070        if (!file.exists()) {
071            throw new FileNotFoundException("Unable to read file " + file.getAbsolutePath() + " for the message");
072        } else {
073            // if the message resist in the new folder its RECENT
074            if (file.getParentFile().getName().equals(MaildirFolder.NEW)) {
075                if (flags == null)
076                    flags = new Flags();
077                flags.add(Flags.Flag.RECENT);
078            }
079        }
080        setFlags(flags);
081        this.messageName = messageName;
082    }
083
084    
085    @Override
086    public Integer getMailboxId() {
087        return mailbox.getMailboxId();
088    }
089
090    @Override
091    public long getUid() {
092        return uid;
093    }
094
095    @Override
096    public void setUid(long uid) {
097        this.uid = uid;
098    }
099    /**
100     * @see
101     * org.apache.james.mailbox.store.mail.model.Message#setFlags(
102     * javax.mail.Flags)
103     */
104    @Override
105    public void setFlags(Flags flags) {
106        if (flags != null) {
107            answered = flags.contains(Flags.Flag.ANSWERED);
108            deleted = flags.contains(Flags.Flag.DELETED);
109            draft = flags.contains(Flags.Flag.DRAFT);
110            flagged = flags.contains(Flags.Flag.FLAGGED);
111            recent = flags.contains(Flags.Flag.RECENT);
112            seen = flags.contains(Flags.Flag.SEEN);
113        }
114    }
115    
116    /**
117     * @see
118     * org.apache.james.mailbox.store.mail.model.Message#isAnswered()
119     */
120    @Override
121    public boolean isAnswered() {
122        return answered;
123    }
124
125    /**
126     * @see
127     * org.apache.james.mailbox.store.mail.model.Message#isDeleted()
128     */
129    @Override
130    public boolean isDeleted() {
131        return deleted;
132    }
133
134    /**
135     * @see
136     * org.apache.james.mailbox.store.mail.model.Message#isDraft()
137     */
138    @Override
139    public boolean isDraft() {
140        return draft;
141    }
142
143    /**
144     * @see
145     * org.apache.james.mailbox.store.mail.model.Message#isFlagged()
146     */
147    @Override
148    public boolean isFlagged() {
149        return flagged;
150    }
151
152    /**
153     * @see
154     * org.apache.james.mailbox.store.mail.model.Message#isRecent()
155     */
156    @Override
157    public boolean isRecent() {
158        return recent;
159    }
160
161    /**
162     * @see org.apache.james.mailbox.store.mail.model.Message#isSeen()
163     */
164    @Override
165    public boolean isSeen() {
166        return seen;
167    }
168
169    /**
170     * Indicates whether this MaildirMessage reflects a new message or one that already
171     * exists in the file system.
172     * @return true if it is new, false if it already exists
173     */
174    public boolean isNew() {
175        return newMessage;
176    }
177    
178    
179    @Override
180    public String toString() {
181        StringBuilder theString = new StringBuilder("MaildirMessage ");
182        theString.append(getUid());
183        theString.append(" {");
184        Flags flags = createFlags();
185        if (flags.contains(Flags.Flag.DRAFT))
186            theString.append(MaildirMessageName.FLAG_DRAFT);
187        if (flags.contains(Flags.Flag.FLAGGED))
188            theString.append(MaildirMessageName.FLAG_FLAGGED);
189        if (flags.contains(Flags.Flag.ANSWERED))
190            theString.append(MaildirMessageName.FLAG_ANSWERD);
191        if (flags.contains(Flags.Flag.SEEN))
192            theString.append(MaildirMessageName.FLAG_SEEN);
193        if (flags.contains(Flags.Flag.DELETED))
194            theString.append(MaildirMessageName.FLAG_DELETED);
195        theString.append("} ");
196        theString.append(getInternalDate());
197        return theString.toString();
198    }
199
200    /**
201     * @see org.apache.james.mailbox.store.mail.model.Message#getModSeq()
202     */
203    @Override
204    public long getModSeq() {
205        return modSeq;
206    }
207
208    /**
209     * @see org.apache.james.mailbox.store.mail.model.Message#setModSeq(long)
210     */
211    @Override
212    public void setModSeq(long modSeq) {
213        this.modSeq = modSeq;
214    }
215    /**
216     * Parse message if needed
217     */
218    private synchronized void parseMessage() {
219        if (parsed)
220            return;
221        SharedFileInputStream tmpMsgIn = null;
222        try {
223            tmpMsgIn = new SharedFileInputStream(messageName.getFile());
224
225            bodyStartOctet = bodyStartOctet(tmpMsgIn);
226
227            // Disable line length... This should be handled by the smtp server
228            // component and not the parser itself
229            // https://issues.apache.org/jira/browse/IMAP-122
230            MimeConfig config = new MimeConfig();
231            config.setMaxLineLen(-1);
232            final MimeTokenStream parser = new MimeTokenStream(config, new DefaultBodyDescriptorBuilder());
233            parser.setRecursionMode(RecursionMode.M_NO_RECURSE);
234            parser.parse(tmpMsgIn.newStream(0, -1));
235
236            EntityState next = parser.next();
237            while (next != EntityState.T_BODY && next != EntityState.T_END_OF_STREAM && next != EntityState.T_START_MULTIPART) {
238                next = parser.next();
239            }
240            final MaximalBodyDescriptor descriptor = (MaximalBodyDescriptor) parser.getBodyDescriptor();
241            final String mediaType;
242            final String mediaTypeFromHeader = descriptor.getMediaType();
243            final String subType;
244            if (mediaTypeFromHeader == null) {
245                mediaType = "text";
246                subType = "plain";
247            } else {
248                mediaType = mediaTypeFromHeader;
249                subType = descriptor.getSubType();
250            }
251            propertyBuilder.setMediaType(mediaType);
252            propertyBuilder.setSubType(subType);
253            propertyBuilder.setContentID(descriptor.getContentId());
254            propertyBuilder.setContentDescription(descriptor.getContentDescription());
255            propertyBuilder.setContentLocation(descriptor.getContentLocation());
256            propertyBuilder.setContentMD5(descriptor.getContentMD5Raw());
257            propertyBuilder.setContentTransferEncoding(descriptor.getTransferEncoding());
258            propertyBuilder.setContentLanguage(descriptor.getContentLanguage());
259            propertyBuilder.setContentDispositionType(descriptor.getContentDispositionType());
260            propertyBuilder.setContentDispositionParameters(descriptor.getContentDispositionParameters());
261            propertyBuilder.setContentTypeParameters(descriptor.getContentTypeParameters());
262            // Add missing types
263            final String codeset = descriptor.getCharset();
264            if (codeset == null) {
265                if ("TEXT".equalsIgnoreCase(mediaType)) {
266                    propertyBuilder.setCharset("us-ascii");
267                }
268            } else {
269                propertyBuilder.setCharset(codeset);
270            }
271
272            final String boundary = descriptor.getBoundary();
273            if (boundary != null) {
274                propertyBuilder.setBoundary(boundary);
275            }
276            if ("text".equalsIgnoreCase(mediaType)) {
277                long lines = -1;
278                final CountingInputStream bodyStream = new CountingInputStream(parser.getInputStream());
279                try {
280                    bodyStream.readAll();
281                    lines = bodyStream.getLineCount();
282                } finally {
283                    IOUtils.closeQuietly(bodyStream);
284                }
285
286                next = parser.next();
287                if (next == EntityState.T_EPILOGUE) {
288                    final CountingInputStream epilogueStream = new CountingInputStream(parser.getInputStream());
289                    try {
290                        epilogueStream.readAll();
291                        lines += epilogueStream.getLineCount();
292                    } finally {
293                        IOUtils.closeQuietly(epilogueStream);
294                    }
295                }
296                propertyBuilder.setTextualLineCount(lines);
297            }
298        } catch (IOException e) {
299            // has successfully been parsen when appending, shouldn't give any
300            // problems
301        } catch (MimeException e) {
302            // has successfully been parsen when appending, shouldn't give any
303            // problems
304        } finally {
305            if (tmpMsgIn != null) {
306                try {
307                    tmpMsgIn.close();
308                } catch (IOException e) {
309                    // ignore on close
310                }
311            }
312            parsed = true;
313        }
314    }
315
316    /**
317     * Return the position in the given {@link InputStream} at which the Body of
318     * the Message starts
319     * 
320     * @param msgIn
321     * @return bodyStartOctet
322     * @throws IOException
323     */
324    private int bodyStartOctet(InputStream msgIn) throws IOException {
325        // we need to pushback maximal 3 bytes
326        PushbackInputStream in = new PushbackInputStream(msgIn, 3);
327        int localBodyStartOctet = in.available();
328        int i = -1;
329        int count = 0;
330        while ((i = in.read()) != -1 && in.available() > 4) {
331            if (i == 0x0D) {
332                int a = in.read();
333                if (a == 0x0A) {
334                    int b = in.read();
335
336                    if (b == 0x0D) {
337                        int c = in.read();
338
339                        if (c == 0x0A) {
340                            localBodyStartOctet = count + 4;
341                            break;
342                        }
343                        in.unread(c);
344                    }
345                    in.unread(b);
346                }
347                in.unread(a);
348            }
349            count++;
350        }
351        return localBodyStartOctet;
352    }
353
354    /**
355     * @see org.apache.james.mailbox.store.mail.model.Message#getMediaType()
356     */
357    @Override
358    public String getMediaType() {
359        parseMessage();
360        return propertyBuilder.getMediaType();
361    }
362
363    /**
364     * @see org.apache.james.mailbox.store.mail.model.Message#getSubType()
365     */
366    @Override
367    public String getSubType() {
368        parseMessage();
369        return propertyBuilder.getSubType();
370    }
371
372    /**
373     * @see org.apache.james.mailbox.store.mail.model.Message#getFullContentOctets()
374     */
375    @Override
376    public long getFullContentOctets() {
377        Long size = messageName.getSize();
378        if (size != null) {
379            return size;
380        } else {
381            try {
382                return messageName.getFile().length();
383            } catch (FileNotFoundException e) {
384                return -1;
385            }
386        }
387    }
388
389    /**
390     * @see org.apache.james.mailbox.store.mail.model.Message#getTextualLineCount()
391     */
392    @Override
393    public Long getTextualLineCount() {
394        parseMessage();
395        return propertyBuilder.getTextualLineCount();
396    }
397
398    /**
399     * @see org.apache.james.mailbox.store.mail.model.Message#getProperties()
400     */
401    @Override
402    public List<Property> getProperties() {
403        parseMessage();
404        return propertyBuilder.toProperties();
405    }
406
407    /**
408     * @see org.apache.james.mailbox.store.mail.model.Message#getInternalDate()
409     */
410    @Override
411    public Date getInternalDate() {
412        return messageName.getInternalDate();
413    }
414
415    /**
416     * Return the full content of the message via a {@link FileInputStream}
417     */
418    @Override
419    public InputStream getFullContent() throws IOException {
420        return new FileInputStream(messageName.getFile());
421    }
422
423    /**
424     * @see org.apache.james.mailbox.store.mail.model.Message#getBodyContent()
425     */
426    @Override
427    public InputStream getBodyContent() throws IOException {
428        parseMessage();
429        FileInputStream body = new FileInputStream(messageName.getFile());
430        IOUtils.skipFully(body, bodyStartOctet);
431        return body;
432
433    }
434
435    /**
436     * @see org.apache.james.mailbox.store.mail.model.AbstractMessage#getBodyStartOctet()
437     */
438    @Override
439    protected int getBodyStartOctet() {
440        parseMessage();
441        return bodyStartOctet;
442    }
443
444    @Override
445    public InputStream getHeaderContent() throws IOException {
446        parseMessage();
447        long limit = getBodyStartOctet();
448        if (limit < 0) {
449            limit = 0;
450        }
451        return new LimitingFileInputStream(messageName.getFile(), limit);
452
453    }
454
455
456}