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 }