2 * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v. 2.0, which is available at
6 * http://www.eclipse.org/legal/epl-2.0.
8 * This Source Code may also be made available under the following Secondary
9 * Licenses when the conditions for such availability set forth in the
10 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11 * version 2 with the GNU Classpath Exception, which is available at
12 * https://www.gnu.org/software/classpath/license.html.
14 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
17 package com
.sun
.mail
.mbox
;
20 import java
.util
.StringTokenizer
;
21 import java
.util
.Date
;
22 import java
.text
.SimpleDateFormat
;
23 import javax
.activation
.*;
25 import javax
.mail
.internet
.*;
26 import javax
.mail
.event
.MessageChangedEvent
;
27 import com
.sun
.mail
.util
.LineInputStream
;
30 * This class represents an RFC822 style email message that resides in a file.
32 * @author Bill Shannon
35 public class MboxMessage
extends MimeMessage
{
37 boolean writable
= false;
38 // original msg flags, used by MboxFolder to detect modification
41 * A UNIX From line looks like:
42 * From address Day Mon DD HH:MM:SS YYYY
45 InternetAddress unix_from_user
;
48 private static OutputStream nullOutputStream
= new OutputStream() {
49 public void write(int b
) { }
50 public void write(byte[] b
, int off
, int len
) { }
54 * Construct an MboxMessage from the InputStream.
56 public MboxMessage(Session session
, InputStream is
)
57 throws MessagingException
, IOException
{
59 BufferedInputStream bis
;
60 if (is
instanceof BufferedInputStream
)
61 bis
= (BufferedInputStream
)is
;
63 bis
= new BufferedInputStream(is
);
64 LineInputStream dis
= new LineInputStream(bis
);
66 String line
= dis
.readLine();
67 if (line
!= null && line
.startsWith("From "))
68 this.unix_from
= line
;
76 * Construct an MboxMessage using the given InternetHeaders object
77 * and content from an InputStream.
79 public MboxMessage(MboxFolder folder
, InternetHeaders hdrs
, InputStream is
,
80 int msgno
, String unix_from
, boolean writable
)
81 throws MessagingException
{
82 super(folder
, hdrs
, null, msgno
);
83 setFlagsFromHeaders();
84 origFlags
= getFlags();
85 this.unix_from
= unix_from
;
86 this.writable
= writable
;
87 this.contentStream
= is
;
91 * Returns the "From" attribute. The "From" attribute contains
92 * the identity of the person(s) who wished this message to
95 * If our superclass doesn't have a value, we return the address
96 * from the UNIX From line.
98 * @return array of Address objects
99 * @exception MessagingException
101 public Address
[] getFrom() throws MessagingException
{
102 Address
[] ret
= super.getFrom();
104 InternetAddress ia
= getUnixFrom();
106 ret
= new InternetAddress
[] { ia
};
112 * Returns the address from the UNIX "From" line.
114 * @return UNIX From address
115 * @exception MessagingException
117 public synchronized InternetAddress
getUnixFrom()
118 throws MessagingException
{
119 if (unix_from_user
== null && unix_from
!= null) {
121 // find the space after the address, before the date
122 i
= unix_from
.indexOf(' ', 5);
126 new InternetAddress(unix_from
.substring(5, i
));
127 } catch (AddressException e
) {
132 return unix_from_user
!= null ?
133 (InternetAddress
)unix_from_user
.clone() : null;
136 private String
getUnixFromLine() {
137 if (unix_from
!= null)
139 String from
= "unknown";
141 Address
[] froma
= getFrom();
142 if (froma
!= null && froma
.length
> 0 &&
143 froma
[0] instanceof InternetAddress
)
144 from
= ((InternetAddress
)froma
[0]).getAddress();
145 } catch (MessagingException ex
) { }
149 } catch (MessagingException ex
) {
154 // From shannon Mon Jun 10 12:06:52 2002
155 SimpleDateFormat fmt
= new SimpleDateFormat("EEE LLL dd HH:mm:ss yyyy");
156 return "From " + from
+ " " + fmt
.format(d
);
160 * Get the date this message was received, from the UNIX From line.
162 * @return the date this message was received
163 * @exception MessagingException
165 @SuppressWarnings("deprecation") // for Date constructor
166 public Date
getReceivedDate() throws MessagingException
{
167 if (rcvDate
== null && unix_from
!= null) {
169 // find the space after the address, before the date
170 i
= unix_from
.indexOf(' ', 5);
173 rcvDate
= new Date(unix_from
.substring(i
));
174 } catch (IllegalArgumentException iae
) {
179 return rcvDate
== null ?
null : new Date(rcvDate
.getTime());
183 * Return the number of lines for the content of this message.
184 * Return -1 if this number cannot be determined. <p>
186 * Note that this number may not be an exact measure of the
187 * content length and may or may not account for any transfer
188 * encoding of the content. <p>
190 * This implementation returns -1.
192 * @return number of lines in the content.
193 * @exception MessagingException
195 public int getLineCount() throws MessagingException
{
196 if (lineCount
< 0 && isMimeType("text/plain")) {
197 LineCounter lc
= null;
198 // writeTo will set the SEEN flag, remember the original state
199 boolean seen
= isSet(Flags
.Flag
.SEEN
);
201 lc
= new LineCounter(nullOutputStream
);
202 getDataHandler().writeTo(lc
);
203 lineCount
= lc
.getLineCount();
204 } catch (IOException ex
) {
205 // ignore it, can't happen
210 } catch (IOException ex
) {
215 setFlag(Flags
.Flag
.SEEN
, false);
221 * Set the specified flags on this message to the specified value.
223 * @param flags the flags to be set
224 * @param set the value to be set
226 public void setFlags(Flags newFlags
, boolean set
)
227 throws MessagingException
{
228 Flags oldFlags
= (Flags
)flags
.clone();
229 super.setFlags(newFlags
, set
);
230 if (!flags
.equals(oldFlags
)) {
231 setHeadersFromFlags(this);
233 ((MboxFolder
)folder
).notifyMessageChangedListeners(
234 MessageChangedEvent
.FLAGS_CHANGED
, this);
239 * Return the content type, mapping from SunV3 types to MIME types
242 public String
getContentType() throws MessagingException
{
243 String ct
= super.getContentType();
244 if (ct
.indexOf('/') < 0)
245 ct
= SunV3BodyPart
.MimeV3Map
.toMime(ct
);
250 * Produce the raw bytes of the content. This method is used during
251 * parsing, to create a DataHandler object for the content. Subclasses
252 * that can provide a separate input stream for just the message
253 * content might want to override this method. <p>
255 * This implementation just returns a ByteArrayInputStream constructed
256 * out of the <code>content</code> byte array.
260 protected InputStream
getContentStream() throws MessagingException
{
262 ((MboxFolder
)folder
).checkOpen();
264 throw new MessageRemovedException("mbox message expunged");
265 if (!isSet(Flags
.Flag
.SEEN
))
266 setFlag(Flags
.Flag
.SEEN
, true);
267 return super.getContentStream();
271 * Return a DataHandler for this Message's content.
272 * If this is a SunV3 multipart message, handle it specially.
274 * @exception MessagingException
276 public synchronized DataHandler
getDataHandler()
277 throws MessagingException
{
279 // XXX - Following is a kludge to avoid having to register
280 // the "multipart/x-sun-attachment" data type with the JAF.
281 String ct
= getContentType();
282 if (ct
.equalsIgnoreCase("multipart/x-sun-attachment"))
283 dh
= new DataHandler(
284 new SunV3Multipart(new MimePartDataSource(this)), ct
);
286 return super.getDataHandler(); // will set "dh"
291 // here only to allow package private access from MboxFolder
292 protected void setMessageNumber(int msgno
) {
293 super.setMessageNumber(msgno
);
296 // here to synchronize access to expunged field
297 public synchronized boolean isExpunged() {
298 return super.isExpunged();
301 // here to synchronize and to allow access from MboxFolder
302 protected synchronized void setExpunged(boolean expunged
) {
303 super.setExpunged(expunged
);
306 // XXX - We assume that only body parts that are part of a SunV3
307 // multipart will use the SunV3 headers (X-Sun-Content-Length,
308 // X-Sun-Content-Lines, X-Sun-Data-Type, X-Sun-Encoding-Info,
309 // X-Sun-Data-Description, X-Sun-Data-Name) so we don't handle
313 * Set the flags for this message based on the Status,
314 * X-Status, and X-Dt-Delete-Time headers.
317 * "X-Status: DFAT", deleted, flagged, answered, draft.
318 * Unset flags represented as "$".
319 * User flags not supported.
321 * University of Washington IMAP server:
322 * "X-Status: DFAT", deleted, flagged, answered, draft.
323 * Unset flags not present.
324 * "X-Keywords: userflag1 userflag2"
326 private synchronized void setFlagsFromHeaders() {
327 flags
= new Flags(Flags
.Flag
.RECENT
);
329 String s
= getHeader("Status", null);
331 if (s
.indexOf('R') >= 0)
332 flags
.add(Flags
.Flag
.SEEN
);
333 if (s
.indexOf('O') >= 0)
334 flags
.remove(Flags
.Flag
.RECENT
);
336 s
= getHeader("X-Dt-Delete-Time", null); // set by dtmail
338 flags
.add(Flags
.Flag
.DELETED
);
339 s
= getHeader("X-Status", null); // set by IMAP server
341 if (s
.indexOf('D') >= 0)
342 flags
.add(Flags
.Flag
.DELETED
);
343 if (s
.indexOf('F') >= 0)
344 flags
.add(Flags
.Flag
.FLAGGED
);
345 if (s
.indexOf('A') >= 0)
346 flags
.add(Flags
.Flag
.ANSWERED
);
347 if (s
.indexOf('T') >= 0)
348 flags
.add(Flags
.Flag
.DRAFT
);
350 s
= getHeader("X-Keywords", null); // set by IMAP server
352 StringTokenizer st
= new StringTokenizer(s
);
353 while (st
.hasMoreTokens())
354 flags
.add(st
.nextToken());
356 } catch (MessagingException e
) {
362 * Set the various header fields that represent the message flags.
364 static void setHeadersFromFlags(MimeMessage msg
) {
366 Flags flags
= msg
.getFlags();
367 StringBuilder status
= new StringBuilder();
368 if (flags
.contains(Flags
.Flag
.SEEN
))
370 if (!flags
.contains(Flags
.Flag
.RECENT
))
372 if (status
.length() > 0)
373 msg
.setHeader("Status", status
.toString());
375 msg
.removeHeader("Status");
377 boolean sims
= false;
378 String s
= msg
.getHeader("X-Status", null);
379 // is it a SIMS 2.0 format X-Status header?
380 sims
= s
!= null && s
.length() == 4 && s
.indexOf('$') >= 0;
382 if (flags
.contains(Flags
.Flag
.DELETED
))
386 if (flags
.contains(Flags
.Flag
.FLAGGED
))
390 if (flags
.contains(Flags
.Flag
.ANSWERED
))
394 if (flags
.contains(Flags
.Flag
.DRAFT
))
398 if (status
.length() > 0)
399 msg
.setHeader("X-Status", status
.toString());
401 msg
.removeHeader("X-Status");
403 String
[] userFlags
= flags
.getUserFlags();
404 if (userFlags
.length
> 0) {
406 for (int i
= 0; i
< userFlags
.length
; i
++)
407 status
.append(userFlags
[i
]).append(' ');
408 status
.setLength(status
.length() - 1); // smash trailing space
409 msg
.setHeader("X-Keywords", status
.toString());
411 if (flags
.contains(Flags
.Flag
.DELETED
)) {
412 s
= msg
.getHeader("X-Dt-Delete-Time", null);
414 // XXX - should be time
415 msg
.setHeader("X-Dt-Delete-Time", "1");
417 } catch (MessagingException e
) {
422 protected void updateHeaders() throws MessagingException
{
423 super.updateHeaders();
424 setHeadersFromFlags(this);
428 * Save any changes made to this message.
430 public void saveChanges() throws MessagingException
{
432 ((MboxFolder
)folder
).checkOpen();
434 throw new MessageRemovedException("mbox message expunged");
436 throw new MessagingException("Message is read-only");
442 * Count the size of the body, in order to set the Content-Length
443 * header. (Should we only do this to update an existing
444 * Content-Length header?)
445 * XXX - We could cache the content bytes here, for use later
448 ContentLengthCounter cos
= new ContentLengthCounter();
449 OutputStream os
= new NewlineOutputStream(cos
);
452 setHeader("Content-Length", String
.valueOf(cos
.getSize()));
453 // setContentSize((int)cos.getSize());
454 } catch (MessagingException e
) {
456 } catch (Exception e
) {
457 throw new MessagingException("unexpected exception " + e
);
462 * Expose modified flag to MboxFolder.
464 boolean isModified() {
469 * Put out a byte stream suitable for saving to a file.
470 * XXX - ultimately implement "ignore headers" here?
472 public void writeToFile(OutputStream os
) throws IOException
{
474 if (getHeader("Content-Length") == null) {
476 * Count the size of the body, in order to set the
477 * Content-Length header.
479 ContentLengthCounter cos
= new ContentLengthCounter();
480 OutputStream oos
= new NewlineOutputStream(cos
);
481 super.writeTo(oos
, null);
483 setHeader("Content-Length", String
.valueOf(cos
.getSize()));
484 // setContentSize((int)cos.getSize());
487 os
= new NewlineOutputStream(os
, true);
488 PrintStream pos
= new PrintStream(os
, false, "iso-8859-1");
490 pos
.println(getUnixFromLine());
491 super.writeTo(pos
, null);
493 } catch (MessagingException e
) {
494 throw new IOException("unexpected exception " + e
);
498 public void writeTo(OutputStream os
, String
[] ignoreList
)
499 throws IOException
, MessagingException
{
500 // set the SEEN flag now, which will normally be set by
501 // getContentStream, so it will show up in our headers
502 if (!isSet(Flags
.Flag
.SEEN
))
503 setFlag(Flags
.Flag
.SEEN
, true);
504 super.writeTo(os
, ignoreList
);
508 * Interpose on superclass method to make sure folder is still open
509 * and message hasn't been expunged.
511 public String
[] getHeader(String name
)
512 throws MessagingException
{
514 ((MboxFolder
)folder
).checkOpen();
516 throw new MessageRemovedException("mbox message expunged");
517 return super.getHeader(name
);
521 * Interpose on superclass method to make sure folder is still open
522 * and message hasn't been expunged.
524 public String
getHeader(String name
, String delimiter
)
525 throws MessagingException
{
527 ((MboxFolder
)folder
).checkOpen();
529 throw new MessageRemovedException("mbox message expunged");
530 return super.getHeader(name
, delimiter
);