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}