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 }