/* * 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.
* * 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.
* * 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.
* * 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.
*
* This implementation just returns a ByteArrayInputStream constructed
* out of the content
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);
}
}