--- /dev/null
+/*
+ * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package com.sun.mail.mbox;
+
+import java.io.*;
+import java.util.StringTokenizer;
+import java.util.Date;
+import java.text.SimpleDateFormat;
+import javax.activation.*;
+import javax.mail.*;
+import javax.mail.internet.*;
+import javax.mail.event.MessageChangedEvent;
+import com.sun.mail.util.LineInputStream;
+
+/**
+ * This class represents an RFC822 style email message that resides in a file.
+ *
+ * @author Bill Shannon
+ */
+
+public class MboxMessage extends MimeMessage {
+
+ boolean writable = false;
+ // original msg flags, used by MboxFolder to detect modification
+ Flags origFlags;
+ /*
+ * A UNIX From line looks like:
+ * From address Day Mon DD HH:MM:SS YYYY
+ */
+ String unix_from;
+ InternetAddress unix_from_user;
+ Date rcvDate;
+ int lineCount = -1;
+ private static OutputStream nullOutputStream = new OutputStream() {
+ public void write(int b) { }
+ public void write(byte[] b, int off, int len) { }
+ };
+
+ /**
+ * Construct an MboxMessage from the InputStream.
+ */
+ public MboxMessage(Session session, InputStream is)
+ throws MessagingException, IOException {
+ super(session);
+ BufferedInputStream bis;
+ if (is instanceof BufferedInputStream)
+ bis = (BufferedInputStream)is;
+ else
+ bis = new BufferedInputStream(is);
+ LineInputStream dis = new LineInputStream(bis);
+ bis.mark(1024);
+ String line = dis.readLine();
+ if (line != null && line.startsWith("From "))
+ this.unix_from = line;
+ else
+ bis.reset();
+ parse(bis);
+ saved = true;
+ }
+
+ /**
+ * Construct an MboxMessage using the given InternetHeaders object
+ * and content from an InputStream.
+ */
+ public MboxMessage(MboxFolder folder, InternetHeaders hdrs, InputStream is,
+ int msgno, String unix_from, boolean writable)
+ throws MessagingException {
+ super(folder, hdrs, null, msgno);
+ setFlagsFromHeaders();
+ origFlags = getFlags();
+ this.unix_from = unix_from;
+ this.writable = writable;
+ this.contentStream = is;
+ }
+
+ /**
+ * Returns the "From" attribute. The "From" attribute contains
+ * the identity of the person(s) who wished this message to
+ * be sent. <p>
+ *
+ * If our superclass doesn't have a value, we return the address
+ * from the UNIX From line.
+ *
+ * @return array of Address objects
+ * @exception MessagingException
+ */
+ public Address[] getFrom() throws MessagingException {
+ Address[] ret = super.getFrom();
+ if (ret == null) {
+ InternetAddress ia = getUnixFrom();
+ if (ia != null)
+ ret = new InternetAddress[] { ia };
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the address from the UNIX "From" line.
+ *
+ * @return UNIX From address
+ * @exception MessagingException
+ */
+ public synchronized InternetAddress getUnixFrom()
+ throws MessagingException {
+ if (unix_from_user == null && unix_from != null) {
+ int i;
+ // find the space after the address, before the date
+ i = unix_from.indexOf(' ', 5);
+ if (i > 5) {
+ try {
+ unix_from_user =
+ new InternetAddress(unix_from.substring(5, i));
+ } catch (AddressException e) {
+ // ignore it
+ }
+ }
+ }
+ return unix_from_user != null ?
+ (InternetAddress)unix_from_user.clone() : null;
+ }
+
+ private String getUnixFromLine() {
+ if (unix_from != null)
+ return unix_from;
+ String from = "unknown";
+ try {
+ Address[] froma = getFrom();
+ if (froma != null && froma.length > 0 &&
+ froma[0] instanceof InternetAddress)
+ from = ((InternetAddress)froma[0]).getAddress();
+ } catch (MessagingException ex) { }
+ Date d = null;
+ try {
+ d = getSentDate();
+ } catch (MessagingException ex) {
+ // ignore
+ }
+ if (d == null)
+ d = new Date();
+ // From shannon Mon Jun 10 12:06:52 2002
+ SimpleDateFormat fmt = new SimpleDateFormat("EEE LLL dd HH:mm:ss yyyy");
+ return "From " + from + " " + fmt.format(d);
+ }
+
+ /**
+ * Get the date this message was received, from the UNIX From line.
+ *
+ * @return the date this message was received
+ * @exception MessagingException
+ */
+ @SuppressWarnings("deprecation") // for Date constructor
+ public Date getReceivedDate() throws MessagingException {
+ if (rcvDate == null && unix_from != null) {
+ int i;
+ // find the space after the address, before the date
+ i = unix_from.indexOf(' ', 5);
+ if (i > 5) {
+ try {
+ rcvDate = new Date(unix_from.substring(i));
+ } catch (IllegalArgumentException iae) {
+ // ignore it
+ }
+ }
+ }
+ return rcvDate == null ? null : new Date(rcvDate.getTime());
+ }
+
+ /**
+ * Return the number of lines for the content of this message.
+ * Return -1 if this number cannot be determined. <p>
+ *
+ * Note that this number may not be an exact measure of the
+ * content length and may or may not account for any transfer
+ * encoding of the content. <p>
+ *
+ * This implementation returns -1.
+ *
+ * @return number of lines in the content.
+ * @exception MessagingException
+ */
+ public int getLineCount() throws MessagingException {
+ if (lineCount < 0 && isMimeType("text/plain")) {
+ LineCounter lc = null;
+ // writeTo will set the SEEN flag, remember the original state
+ boolean seen = isSet(Flags.Flag.SEEN);
+ try {
+ lc = new LineCounter(nullOutputStream);
+ getDataHandler().writeTo(lc);
+ lineCount = lc.getLineCount();
+ } catch (IOException ex) {
+ // ignore it, can't happen
+ } finally {
+ try {
+ if (lc != null)
+ lc.close();
+ } catch (IOException ex) {
+ // can't happen
+ }
+ }
+ if (!seen)
+ setFlag(Flags.Flag.SEEN, false);
+ }
+ return lineCount;
+ }
+
+ /**
+ * Set the specified flags on this message to the specified value.
+ *
+ * @param flags the flags to be set
+ * @param set the value to be set
+ */
+ public void setFlags(Flags newFlags, boolean set)
+ throws MessagingException {
+ Flags oldFlags = (Flags)flags.clone();
+ super.setFlags(newFlags, set);
+ if (!flags.equals(oldFlags)) {
+ setHeadersFromFlags(this);
+ if (folder != null)
+ ((MboxFolder)folder).notifyMessageChangedListeners(
+ MessageChangedEvent.FLAGS_CHANGED, this);
+ }
+ }
+
+ /**
+ * Return the content type, mapping from SunV3 types to MIME types
+ * as necessary.
+ */
+ public String getContentType() throws MessagingException {
+ String ct = super.getContentType();
+ if (ct.indexOf('/') < 0)
+ ct = SunV3BodyPart.MimeV3Map.toMime(ct);
+ return ct;
+ }
+
+ /**
+ * Produce the raw bytes of the content. This method is used during
+ * parsing, to create a DataHandler object for the content. Subclasses
+ * that can provide a separate input stream for just the message
+ * content might want to override this method. <p>
+ *
+ * This implementation just returns a ByteArrayInputStream constructed
+ * out of the <code>content</code> byte array.
+ *
+ * @see #content
+ */
+ protected InputStream getContentStream() throws MessagingException {
+ if (folder != null)
+ ((MboxFolder)folder).checkOpen();
+ if (isExpunged())
+ throw new MessageRemovedException("mbox message expunged");
+ if (!isSet(Flags.Flag.SEEN))
+ setFlag(Flags.Flag.SEEN, true);
+ return super.getContentStream();
+ }
+
+ /**
+ * Return a DataHandler for this Message's content.
+ * If this is a SunV3 multipart message, handle it specially.
+ *
+ * @exception MessagingException
+ */
+ public synchronized DataHandler getDataHandler()
+ throws MessagingException {
+ if (dh == null) {
+ // XXX - Following is a kludge to avoid having to register
+ // the "multipart/x-sun-attachment" data type with the JAF.
+ String ct = getContentType();
+ if (ct.equalsIgnoreCase("multipart/x-sun-attachment"))
+ dh = new DataHandler(
+ new SunV3Multipart(new MimePartDataSource(this)), ct);
+ else
+ return super.getDataHandler(); // will set "dh"
+ }
+ return dh;
+ }
+
+ // here only to allow package private access from MboxFolder
+ protected void setMessageNumber(int msgno) {
+ super.setMessageNumber(msgno);
+ }
+
+ // here to synchronize access to expunged field
+ public synchronized boolean isExpunged() {
+ return super.isExpunged();
+ }
+
+ // here to synchronize and to allow access from MboxFolder
+ protected synchronized void setExpunged(boolean expunged) {
+ super.setExpunged(expunged);
+ }
+
+ // XXX - We assume that only body parts that are part of a SunV3
+ // multipart will use the SunV3 headers (X-Sun-Content-Length,
+ // X-Sun-Content-Lines, X-Sun-Data-Type, X-Sun-Encoding-Info,
+ // X-Sun-Data-Description, X-Sun-Data-Name) so we don't handle
+ // them here.
+
+ /**
+ * Set the flags for this message based on the Status,
+ * X-Status, and X-Dt-Delete-Time headers.
+ *
+ * SIMS 2.0:
+ * "X-Status: DFAT", deleted, flagged, answered, draft.
+ * Unset flags represented as "$".
+ * User flags not supported.
+ *
+ * University of Washington IMAP server:
+ * "X-Status: DFAT", deleted, flagged, answered, draft.
+ * Unset flags not present.
+ * "X-Keywords: userflag1 userflag2"
+ */
+ private synchronized void setFlagsFromHeaders() {
+ flags = new Flags(Flags.Flag.RECENT);
+ try {
+ String s = getHeader("Status", null);
+ if (s != null) {
+ if (s.indexOf('R') >= 0)
+ flags.add(Flags.Flag.SEEN);
+ if (s.indexOf('O') >= 0)
+ flags.remove(Flags.Flag.RECENT);
+ }
+ s = getHeader("X-Dt-Delete-Time", null); // set by dtmail
+ if (s != null)
+ flags.add(Flags.Flag.DELETED);
+ s = getHeader("X-Status", null); // set by IMAP server
+ if (s != null) {
+ if (s.indexOf('D') >= 0)
+ flags.add(Flags.Flag.DELETED);
+ if (s.indexOf('F') >= 0)
+ flags.add(Flags.Flag.FLAGGED);
+ if (s.indexOf('A') >= 0)
+ flags.add(Flags.Flag.ANSWERED);
+ if (s.indexOf('T') >= 0)
+ flags.add(Flags.Flag.DRAFT);
+ }
+ s = getHeader("X-Keywords", null); // set by IMAP server
+ if (s != null) {
+ StringTokenizer st = new StringTokenizer(s);
+ while (st.hasMoreTokens())
+ flags.add(st.nextToken());
+ }
+ } catch (MessagingException e) {
+ // ignore it
+ }
+ }
+
+ /**
+ * Set the various header fields that represent the message flags.
+ */
+ static void setHeadersFromFlags(MimeMessage msg) {
+ try {
+ Flags flags = msg.getFlags();
+ StringBuilder status = new StringBuilder();
+ if (flags.contains(Flags.Flag.SEEN))
+ status.append('R');
+ if (!flags.contains(Flags.Flag.RECENT))
+ status.append('O');
+ if (status.length() > 0)
+ msg.setHeader("Status", status.toString());
+ else
+ msg.removeHeader("Status");
+
+ boolean sims = false;
+ String s = msg.getHeader("X-Status", null);
+ // is it a SIMS 2.0 format X-Status header?
+ sims = s != null && s.length() == 4 && s.indexOf('$') >= 0;
+ status.setLength(0);
+ if (flags.contains(Flags.Flag.DELETED))
+ status.append('D');
+ else if (sims)
+ status.append('$');
+ if (flags.contains(Flags.Flag.FLAGGED))
+ status.append('F');
+ else if (sims)
+ status.append('$');
+ if (flags.contains(Flags.Flag.ANSWERED))
+ status.append('A');
+ else if (sims)
+ status.append('$');
+ if (flags.contains(Flags.Flag.DRAFT))
+ status.append('T');
+ else if (sims)
+ status.append('$');
+ if (status.length() > 0)
+ msg.setHeader("X-Status", status.toString());
+ else
+ msg.removeHeader("X-Status");
+
+ String[] userFlags = flags.getUserFlags();
+ if (userFlags.length > 0) {
+ status.setLength(0);
+ for (int i = 0; i < userFlags.length; i++)
+ status.append(userFlags[i]).append(' ');
+ status.setLength(status.length() - 1); // smash trailing space
+ msg.setHeader("X-Keywords", status.toString());
+ }
+ if (flags.contains(Flags.Flag.DELETED)) {
+ s = msg.getHeader("X-Dt-Delete-Time", null);
+ if (s == null)
+ // XXX - should be time
+ msg.setHeader("X-Dt-Delete-Time", "1");
+ }
+ } catch (MessagingException e) {
+ // ignore it
+ }
+ }
+
+ protected void updateHeaders() throws MessagingException {
+ super.updateHeaders();
+ setHeadersFromFlags(this);
+ }
+
+ /**
+ * Save any changes made to this message.
+ */
+ public void saveChanges() throws MessagingException {
+ if (folder != null)
+ ((MboxFolder)folder).checkOpen();
+ if (isExpunged())
+ throw new MessageRemovedException("mbox message expunged");
+ if (!writable)
+ throw new MessagingException("Message is read-only");
+
+ super.saveChanges();
+
+ try {
+ /*
+ * Count the size of the body, in order to set the Content-Length
+ * header. (Should we only do this to update an existing
+ * Content-Length header?)
+ * XXX - We could cache the content bytes here, for use later
+ * in writeTo.
+ */
+ ContentLengthCounter cos = new ContentLengthCounter();
+ OutputStream os = new NewlineOutputStream(cos);
+ super.writeTo(os);
+ os.flush();
+ setHeader("Content-Length", String.valueOf(cos.getSize()));
+ // setContentSize((int)cos.getSize());
+ } catch (MessagingException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new MessagingException("unexpected exception " + e);
+ }
+ }
+
+ /**
+ * Expose modified flag to MboxFolder.
+ */
+ boolean isModified() {
+ return modified;
+ }
+
+ /**
+ * Put out a byte stream suitable for saving to a file.
+ * XXX - ultimately implement "ignore headers" here?
+ */
+ public void writeToFile(OutputStream os) throws IOException {
+ try {
+ if (getHeader("Content-Length") == null) {
+ /*
+ * Count the size of the body, in order to set the
+ * Content-Length header.
+ */
+ ContentLengthCounter cos = new ContentLengthCounter();
+ OutputStream oos = new NewlineOutputStream(cos);
+ super.writeTo(oos, null);
+ oos.flush();
+ setHeader("Content-Length", String.valueOf(cos.getSize()));
+ // setContentSize((int)cos.getSize());
+ }
+
+ os = new NewlineOutputStream(os, true);
+ PrintStream pos = new PrintStream(os, false, "iso-8859-1");
+
+ pos.println(getUnixFromLine());
+ super.writeTo(pos, null);
+ pos.flush();
+ } catch (MessagingException e) {
+ throw new IOException("unexpected exception " + e);
+ }
+ }
+
+ public void writeTo(OutputStream os, String[] ignoreList)
+ throws IOException, MessagingException {
+ // set the SEEN flag now, which will normally be set by
+ // getContentStream, so it will show up in our headers
+ if (!isSet(Flags.Flag.SEEN))
+ setFlag(Flags.Flag.SEEN, true);
+ super.writeTo(os, ignoreList);
+ }
+
+ /**
+ * Interpose on superclass method to make sure folder is still open
+ * and message hasn't been expunged.
+ */
+ public String[] getHeader(String name)
+ throws MessagingException {
+ if (folder != null)
+ ((MboxFolder)folder).checkOpen();
+ if (isExpunged())
+ throw new MessageRemovedException("mbox message expunged");
+ return super.getHeader(name);
+ }
+
+ /**
+ * Interpose on superclass method to make sure folder is still open
+ * and message hasn't been expunged.
+ */
+ public String getHeader(String name, String delimiter)
+ throws MessagingException {
+ if (folder != null)
+ ((MboxFolder)folder).checkOpen();
+ if (isExpunged())
+ throw new MessageRemovedException("mbox message expunged");
+ return super.getHeader(name, delimiter);
+ }
+}