001    /*
002     * Copyright 2010 the original author or authors.
003     * 
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package com.hs.mail.imap.message.response;
017    
018    import java.io.BufferedInputStream;
019    import java.io.File;
020    import java.io.FileInputStream;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.nio.ByteBuffer;
024    import java.util.HashMap;
025    import java.util.Map;
026    import java.util.zip.GZIPInputStream;
027    
028    import javax.mail.FetchProfile;
029    import javax.mail.Flags;
030    
031    import org.apache.commons.io.IOUtils;
032    import org.apache.commons.lang.ArrayUtils;
033    import org.apache.james.mime4j.MimeException;
034    import org.apache.log4j.Logger;
035    import org.springframework.core.io.ClassPathResource;
036    
037    import com.hs.mail.container.config.Config;
038    import com.hs.mail.imap.mailbox.MailboxManager;
039    import com.hs.mail.imap.message.BodyFetchItem;
040    import com.hs.mail.imap.message.FetchData;
041    import com.hs.mail.imap.processor.fetch.BodyStructureBuilder;
042    import com.hs.mail.imap.processor.fetch.Content;
043    import com.hs.mail.imap.processor.fetch.Envelope;
044    import com.hs.mail.imap.processor.fetch.EnvelopeBuilder;
045    import com.hs.mail.imap.processor.fetch.MimeDescriptor;
046    import com.hs.mail.imap.processor.fetch.PartContentBuilder;
047    import com.hs.mail.util.FileUtils;
048    
049    /**
050     * 
051     * @author Won Chul Doh
052     * @since Mar 8, 2010
053     *
054     */
055    public class FetchResponseBuilder {
056            
057            private static Logger logger = Logger.getLogger(FetchResponseBuilder.class);
058            
059            private MailboxManager manager;
060            private EnvelopeBuilder envelopeBuilder;
061            private BodyStructureBuilder bodyStructureBuilder;
062    
063            public FetchResponseBuilder(MailboxManager manager) {
064                    this.manager = manager;
065                    this.envelopeBuilder = new EnvelopeBuilder();
066                    this.bodyStructureBuilder = new BodyStructureBuilder(this.envelopeBuilder);
067            }
068            
069            public FetchResponse build(long msgnum, FetchProfile fp, FetchData fd) {
070                    FetchResponse response = new FetchResponse(msgnum);
071                    if (fp.contains(FetchProfile.Item.FLAGS)) {
072                            response.setFlags(fd.getFlags());
073                    }
074                    if (fp.contains(FetchData.FetchProfileItem.INTERNALDATE)) {
075                            response.setInternalDate(fd.getInternalDate());
076                    }
077                    if (fp.contains(FetchData.FetchProfileItem.SIZE)) {
078                            response.setSize(new Long(fd.getSize()));
079                    }
080                    if (fp.contains(FetchProfile.Item.ENVELOPE)) {
081                            Envelope envelope = buildEnvelope(fd.getPhysMessageID());
082                            response.setEnvelope(envelope);
083                    }
084                    if (fp.contains(FetchData.FetchProfileItem.BODY)
085                                    || fp.contains(FetchData.FetchProfileItem.BODYSTRUCTURE)) {
086                            MimeDescriptor descriptor = buildBodyStructure(fd);
087                            if (fp.contains(FetchData.FetchProfileItem.BODY)) {
088                                    response.setBody(descriptor);
089                            }
090                            if (fp.contains(FetchData.FetchProfileItem.BODYSTRUCTURE)) {
091                                    response.setBodyStructure(descriptor);
092                            }
093                    }
094                    if (fp.contains(FetchData.FetchProfileItem.UID)) {
095                            response.setUid(new Long(fd.getMessageID()));
096                    }
097                    BodyFetchItem item = getBodyFetchItem(fp);
098                    if (item != null) {
099                            try {
100                                    byte[] contents = bodyFetch(fd, item);
101                                    Content content = buildBodyContent(contents, item);
102                                    response.setContent(content);
103                                    // Check if this fetch will cause the "SEEN" flag to be set
104                                    // on this message.
105                                    if (!item.isPeek()) {
106                                            if (fd.getFlags() == null
107                                                            || !fd.getFlags().contains(Flags.Flag.SEEN)) {
108                                                    manager.setFlags(fd.getMessageID(), new Flags(
109                                                                    Flags.Flag.SEEN), false, true);
110                                            }
111                                    }
112                            } catch (Exception e) {
113                                    // FIXME The main reason for this exception is that the message
114                                    // file is not exist.
115                                    // If we throw exception, all subsequent fetch will fail.
116                                    // So, ignore this exception at now.
117                            }
118                    }
119                    return response;
120            }
121            
122            private BodyFetchItem getBodyFetchItem(FetchProfile fp) {
123                    FetchProfile.Item[] items = fp.getItems();
124                    BodyFetchItem result = null;
125                    if (!ArrayUtils.isEmpty(items)) {
126                            for (int i = 0; i < items.length; i++) {
127                                    if (items[i] instanceof BodyFetchItem) {
128                                            result = (BodyFetchItem) items[i];
129                                            break;
130                                    }
131                            }
132                    }
133                    return result;
134            }
135            
136            private Map<String, String> getHeader(long physmessageid) {
137                    return manager.getHeader(physmessageid);
138            }
139            
140            private Map<String, String> getHeader(long physmessageid, String[] fields) {
141                    return manager.getHeader(physmessageid, fields);
142            }
143    
144            private Envelope buildEnvelope(long physmessageid) {
145                    Map<String, String> header = getHeader(physmessageid,
146                                    EnvelopeBuilder.WANTED_FIELDS);
147                    return envelopeBuilder.build(header);
148            }
149            
150            private static InputStream getInputStream(FetchData fd) throws IOException {
151                    File file = Config.getDataFile(fd.getInternalDate(),
152                                    fd.getPhysMessageID());
153                    if (FileUtils.isCompressed(file, false)) {
154                            return new GZIPInputStream(new FileInputStream(file));
155                    } else if (file.exists()) {
156                            return new BufferedInputStream(new FileInputStream(file));
157                    } else {
158                            return new ClassPathResource("/META-INF/notexist.eml").getInputStream();
159                    }
160            }
161    
162            private MimeDescriptor buildBodyStructure(FetchData fd) {
163                    InputStream is = null;
164                    try {
165                            is = getInputStream(fd);
166                            MimeDescriptor descriptor = bodyStructureBuilder.build(is);
167                            return descriptor;
168                    } catch (Exception e) {
169                            logger.error(e.getMessage(), e);
170                            return null;
171                    } finally {
172                            IOUtils.closeQuietly(is);
173                    }
174            }
175            
176            private Content buildBodyContent(byte[] bytes, BodyFetchItem item) {
177                    long firstOctet = item.getFirstOctet();
178                    if (firstOctet >= 0) {
179                            long numberOfOctets = item.getNumberOfOctets();
180                            return buildPartialBodyContent(item.getName(), bytes, firstOctet,
181                                            numberOfOctets);
182                    } else {
183                            return new Content(item.getName(), ByteBuffer.wrap(bytes));
184                    }
185            }
186            
187            private Content buildPartialBodyContent(String name, byte[] bytes,
188                            long firstOctet, long numberOfOctets) {
189                    if (firstOctet >= bytes.length) {
190                            // If the starting octet is beyond the end of the text, an empty
191                            // string is returned.
192                            return new Content(name, ByteBuffer.wrap(PartContentBuilder.EMPTY));
193                    } else {
194                            if ((numberOfOctets < 0)
195                                            || ((firstOctet + numberOfOctets) > bytes.length)) {
196                                    // If attempt to read beyond the end of the text, truncate as
197                                    // appropriate.
198                                    numberOfOctets = bytes.length - firstOctet;
199                            }
200                            StringBuilder sb = new StringBuilder(name).append('<').append(
201                                            firstOctet).append('>');
202                            return new Content(sb.toString(), ByteBuffer.wrap(bytes,
203                                            (int) firstOctet, (int) numberOfOctets), numberOfOctets);
204                    }
205            }
206            
207            private byte[] bodyFetch(FetchData fd, BodyFetchItem item)
208                            throws IOException, MimeException {
209                    int[] path = item.getPath();
210                    try {
211                            if (ArrayUtils.isEmpty(path)) {
212                                    return bodyContent(fd, item);
213                            } else {
214                                    return bodyContent(fd, item, path, false);
215                            }
216                    } catch (PartContentBuilder.PartNotFoundException e) {
217                            // Missing parts should return zero sized content
218                            return PartContentBuilder.EMPTY;
219                    }
220            }
221            
222            private byte[] bodyContent(FetchData fd, BodyFetchItem item)
223                            throws IOException, MimeException {
224                    long physmessageid = fd.getPhysMessageID();
225                    int specifier = item.getSectionType();
226                    String[] fields = item.getHeaders();
227                    switch (specifier) {
228                    case BodyFetchItem.HEADER:
229                    case BodyFetchItem.MIME:
230                            return addHeader(getHeader(physmessageid));
231                    case BodyFetchItem.HEADER_FIELDS:
232                            return addHeader(getHeader(physmessageid, fields), fields, false);
233                    case BodyFetchItem.HEADER_FIELDS_NOT:
234                            return addHeader(getHeader(physmessageid), fields, true);
235                    default:
236                            return bodyContent(fd, item, null, true);
237                    }
238            }
239    
240            private byte[] bodyContent(FetchData fd, BodyFetchItem item, int[] path,
241                            boolean isBase) throws IOException, MimeException {
242                    InputStream is = null;
243                    try {
244                            is = getInputStream(fd);
245                            return bodyContent(is, item, path, isBase);
246                    } finally {
247                            IOUtils.closeQuietly(is);
248                    }
249            }
250            
251            private byte[] bodyContent(InputStream is, BodyFetchItem item, int[] path,
252                            boolean isBase) throws IOException, MimeException {
253                    int specifier = item.getSectionType();
254                    String[] fields = item.getHeaders();
255                    switch (specifier) {
256                    case BodyFetchItem.HEADER:
257                            return addHeader(getHeader(is, path));
258                    case BodyFetchItem.HEADER_FIELDS:
259                            return addHeader(getHeader(is, path), fields, false);
260                    case BodyFetchItem.HEADER_FIELDS_NOT:
261                            return addHeader(getHeader(is, path), fields, true);
262                    case BodyFetchItem.MIME:
263                            return addHeader(getMimeHeader(is, path));
264                    case BodyFetchItem.TEXT:
265                            return addBodyContent(is, path);
266                    case BodyFetchItem.CONTENT:
267                    default:
268                            return addMimeBodyContent(is, path, isBase);
269                    }
270            }
271            
272            private boolean contains(String[] fields, String name) {
273                    for (int i = 0; i < fields.length; i++) {
274                            if (fields[i].equalsIgnoreCase(name)) {
275                                    return true;
276                            }
277                    }
278                    return false;
279            }
280            
281            private void matching(String[] fields, Map<String, String> header,
282                            boolean not) {
283                    Object[] keys = header.keySet().toArray();
284                    for (int i = 0; i < keys.length; i++) {
285                            boolean match = contains(fields, (String) keys[i]);
286                            if ((!not && !match) || (not && match)) {
287                                    header.remove(keys[i]);
288                            }
289                    }
290            }
291            
292            private Map<String, String> getHeader(InputStream is, int[] path)
293                            throws IOException, MimeException {
294                    PartContentBuilder builder = new PartContentBuilder();
295                    builder.build(is, path);
296                    Map<String, String> header = builder.getMessageHeader();
297                    return header;
298            }
299    
300            private Map<String, String> getMimeHeader(InputStream is, int[] path)
301                            throws IOException, MimeException {
302                    PartContentBuilder builder = new PartContentBuilder();
303                    builder.build(is, path);
304                    return builder.getMimeHeader();
305            }
306    
307            private byte[] addHeader(Map<String, String> header) {
308                    return toBytes(header);
309            }
310    
311            private byte[] addHeader(Map<String, String> header, String[] fields,
312                            boolean not) {
313                    Map<String, String> clone = new HashMap<String, String>(header);
314                    matching(fields, clone, not);
315                    return toBytes(clone);
316            }
317    
318            private byte[] addBodyContent(InputStream is, int[] path)
319                            throws IOException, MimeException {
320                    PartContentBuilder builder = new PartContentBuilder();
321                    builder.build(is, path);
322                    return builder.getMessageBodyContent();
323            }
324    
325            private byte[] addMimeBodyContent(InputStream is, int[] path, boolean isBase)
326                            throws IOException, MimeException {
327                    if (isBase) {
328                            return IOUtils.toByteArray(is);
329                    } else {
330                            PartContentBuilder builder = new PartContentBuilder();
331                            builder.build(is, path);
332                            return builder.getMimeBodyContent();
333                    }
334            }
335            
336            private byte[] toBytes(Map<String, String> header) {
337                    StringBuilder sb = new StringBuilder();
338                    for (Map.Entry<String, String> entry : header.entrySet()) {
339                            String value = entry.getValue(); 
340                            if (value != null) {
341                                    sb.append(entry.getKey()).append(": ").append(entry.getValue())
342                                                    .append("\r\n");
343                            }
344                    }
345                    sb.append("\r\n");
346                    return sb.toString().getBytes();
347            }
348            
349    }