Skip to content

Package: MboxMessage$1

MboxMessage$1

nameinstructionbranchcomplexitylinemethod
write(byte[], int, int)
M: 1 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
write(int)
M: 1 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
{...}
M: 3 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%

Coverage

1: /*
2: * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
3: *
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.
7: *
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.
13: *
14: * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15: */
16:
17: package org.eclipse.angus.mail.mbox;
18:
19: import jakarta.activation.DataHandler;
20: import jakarta.mail.Address;
21: import jakarta.mail.Flags;
22: import jakarta.mail.MessageRemovedException;
23: import jakarta.mail.MessagingException;
24: import jakarta.mail.Session;
25: import jakarta.mail.event.MessageChangedEvent;
26: import jakarta.mail.internet.AddressException;
27: import jakarta.mail.internet.InternetAddress;
28: import jakarta.mail.internet.InternetHeaders;
29: import jakarta.mail.internet.MimeMessage;
30: import jakarta.mail.internet.MimePartDataSource;
31: import org.eclipse.angus.mail.util.LineInputStream;
32:
33: import java.io.BufferedInputStream;
34: import java.io.IOException;
35: import java.io.InputStream;
36: import java.io.OutputStream;
37: import java.io.PrintStream;
38: import java.text.SimpleDateFormat;
39: import java.util.Date;
40: import java.util.StringTokenizer;
41:
42: /**
43: * This class represents an RFC822 style email message that resides in a file.
44: *
45: * @author Bill Shannon
46: */
47:
48: public class MboxMessage extends MimeMessage {
49:
50: boolean writable = false;
51: // original msg flags, used by MboxFolder to detect modification
52: Flags origFlags;
53: /*
54: * A UNIX From line looks like:
55: * From address Day Mon DD HH:MM:SS YYYY
56: */
57: String unix_from;
58: InternetAddress unix_from_user;
59: Date rcvDate;
60: int lineCount = -1;
61: private static OutputStream nullOutputStream = new OutputStream() {
62: public void write(int b) {
63: }
64:
65: public void write(byte[] b, int off, int len) {
66: }
67: };
68:
69: /**
70: * Construct an MboxMessage from the InputStream.
71: */
72: public MboxMessage(Session session, InputStream is)
73: throws MessagingException, IOException {
74: super(session);
75: BufferedInputStream bis;
76: if (is instanceof BufferedInputStream)
77: bis = (BufferedInputStream) is;
78: else
79: bis = new BufferedInputStream(is);
80: LineInputStream dis = new LineInputStream(bis);
81: bis.mark(1024);
82: String line = dis.readLine();
83: if (line != null && line.startsWith("From "))
84: this.unix_from = line;
85: else
86: bis.reset();
87: parse(bis);
88: saved = true;
89: }
90:
91: /**
92: * Construct an MboxMessage using the given InternetHeaders object
93: * and content from an InputStream.
94: */
95: public MboxMessage(MboxFolder folder, InternetHeaders hdrs, InputStream is,
96: int msgno, String unix_from, boolean writable)
97: throws MessagingException {
98: super(folder, hdrs, null, msgno);
99: setFlagsFromHeaders();
100: origFlags = getFlags();
101: this.unix_from = unix_from;
102: this.writable = writable;
103: this.contentStream = is;
104: }
105:
106: /**
107: * Returns the "From" attribute. The "From" attribute contains
108: * the identity of the person(s) who wished this message to
109: * be sent. <p>
110: *
111: * If our superclass doesn't have a value, we return the address
112: * from the UNIX From line.
113: *
114: * @return array of Address objects
115: */
116: public Address[] getFrom() throws MessagingException {
117: Address[] ret = super.getFrom();
118: if (ret == null) {
119: InternetAddress ia = getUnixFrom();
120: if (ia != null)
121: ret = new InternetAddress[]{ia};
122: }
123: return ret;
124: }
125:
126: /**
127: * Returns the address from the UNIX "From" line.
128: *
129: * @return UNIX From address
130: */
131: public synchronized InternetAddress getUnixFrom()
132: throws MessagingException {
133: if (unix_from_user == null && unix_from != null) {
134: int i;
135: // find the space after the address, before the date
136: i = unix_from.indexOf(' ', 5);
137: if (i > 5) {
138: try {
139: unix_from_user =
140: new InternetAddress(unix_from.substring(5, i));
141: } catch (AddressException e) {
142: // ignore it
143: }
144: }
145: }
146: return unix_from_user != null ?
147: (InternetAddress) unix_from_user.clone() : null;
148: }
149:
150: private String getUnixFromLine() {
151: if (unix_from != null)
152: return unix_from;
153: String from = "unknown";
154: try {
155: Address[] froma = getFrom();
156: if (froma != null && froma.length > 0 &&
157: froma[0] instanceof InternetAddress)
158: from = ((InternetAddress) froma[0]).getAddress();
159: } catch (MessagingException ex) {
160: }
161: Date d = null;
162: try {
163: d = getSentDate();
164: } catch (MessagingException ex) {
165: // ignore
166: }
167: if (d == null)
168: d = new Date();
169: // From shannon Mon Jun 10 12:06:52 2002
170: SimpleDateFormat fmt = new SimpleDateFormat("EEE LLL dd HH:mm:ss yyyy");
171: return "From " + from + " " + fmt.format(d);
172: }
173:
174: /**
175: * Get the date this message was received, from the UNIX From line.
176: *
177: * @return the date this message was received
178: */
179: @SuppressWarnings("deprecation") // for Date constructor
180: public Date getReceivedDate() throws MessagingException {
181: if (rcvDate == null && unix_from != null) {
182: int i;
183: // find the space after the address, before the date
184: i = unix_from.indexOf(' ', 5);
185: if (i > 5) {
186: try {
187: rcvDate = new Date(unix_from.substring(i));
188: } catch (IllegalArgumentException iae) {
189: // ignore it
190: }
191: }
192: }
193: return rcvDate == null ? null : new Date(rcvDate.getTime());
194: }
195:
196: /**
197: * Return the number of lines for the content of this message.
198: * Return -1 if this number cannot be determined. <p>
199: *
200: * Note that this number may not be an exact measure of the
201: * content length and may or may not account for any transfer
202: * encoding of the content. <p>
203: *
204: * This implementation returns -1.
205: *
206: * @return number of lines in the content.
207: * @exception MessagingException
208: */
209: public int getLineCount() throws MessagingException {
210: if (lineCount < 0 && isMimeType("text/plain")) {
211: LineCounter lc = null;
212: // writeTo will set the SEEN flag, remember the original state
213: boolean seen = isSet(Flags.Flag.SEEN);
214: try {
215: lc = new LineCounter(nullOutputStream);
216: getDataHandler().writeTo(lc);
217: lineCount = lc.getLineCount();
218: } catch (IOException ex) {
219: // ignore it, can't happen
220: } finally {
221: try {
222: if (lc != null)
223: lc.close();
224: } catch (IOException ex) {
225: // can't happen
226: }
227: }
228: if (!seen)
229: setFlag(Flags.Flag.SEEN, false);
230: }
231: return lineCount;
232: }
233:
234: /**
235: * Set the specified flags on this message to the specified value.
236: *
237: * @param newFlags the flags to be set
238: * @param set the value to be set
239: */
240: public void setFlags(Flags newFlags, boolean set)
241: throws MessagingException {
242: Flags oldFlags = (Flags) flags.clone();
243: super.setFlags(newFlags, set);
244: if (!flags.equals(oldFlags)) {
245: setHeadersFromFlags(this);
246: if (folder != null)
247: ((MboxFolder) folder).notifyMessageChangedListeners(
248: MessageChangedEvent.FLAGS_CHANGED, this);
249: }
250: }
251:
252: /**
253: * Return the content type, mapping from SunV3 types to MIME types
254: * as necessary.
255: */
256: public String getContentType() throws MessagingException {
257: String ct = super.getContentType();
258: if (ct.indexOf('/') < 0)
259: ct = SunV3BodyPart.MimeV3Map.toMime(ct);
260: return ct;
261: }
262:
263: /**
264: * Produce the raw bytes of the content. This method is used during
265: * parsing, to create a DataHandler object for the content. Subclasses
266: * that can provide a separate input stream for just the message
267: * content might want to override this method. <p>
268: *
269: * This implementation just returns a ByteArrayInputStream constructed
270: * out of the <code>content</code> byte array.
271: *
272: * @see #content
273: */
274: protected InputStream getContentStream() throws MessagingException {
275: if (folder != null)
276: ((MboxFolder) folder).checkOpen();
277: if (isExpunged())
278: throw new MessageRemovedException("mbox message expunged");
279: if (!isSet(Flags.Flag.SEEN))
280: setFlag(Flags.Flag.SEEN, true);
281: return super.getContentStream();
282: }
283:
284: /**
285: * Return a DataHandler for this Message's content.
286: * If this is a SunV3 multipart message, handle it specially.
287: *
288: * @exception MessagingException
289: */
290: public synchronized DataHandler getDataHandler()
291: throws MessagingException {
292: if (dh == null) {
293: // XXX - Following is a kludge to avoid having to register
294: // the "multipart/x-sun-attachment" data type with the JAF.
295: String ct = getContentType();
296: if (ct.equalsIgnoreCase("multipart/x-sun-attachment"))
297: dh = new DataHandler(
298: new SunV3Multipart(new MimePartDataSource(this)), ct);
299: else
300: return super.getDataHandler(); // will set "dh"
301: }
302: return dh;
303: }
304:
305: // here only to allow package private access from MboxFolder
306: protected void setMessageNumber(int msgno) {
307: super.setMessageNumber(msgno);
308: }
309:
310: // here to synchronize access to expunged field
311: public synchronized boolean isExpunged() {
312: return super.isExpunged();
313: }
314:
315: // here to synchronize and to allow access from MboxFolder
316: protected synchronized void setExpunged(boolean expunged) {
317: super.setExpunged(expunged);
318: }
319:
320: // XXX - We assume that only body parts that are part of a SunV3
321: // multipart will use the SunV3 headers (X-Sun-Content-Length,
322: // X-Sun-Content-Lines, X-Sun-Data-Type, X-Sun-Encoding-Info,
323: // X-Sun-Data-Description, X-Sun-Data-Name) so we don't handle
324: // them here.
325:
326: /**
327: * Set the flags for this message based on the Status,
328: * X-Status, and X-Dt-Delete-Time headers.
329: *
330: * SIMS 2.0:
331: * "X-Status: DFAT", deleted, flagged, answered, draft.
332: * Unset flags represented as "$".
333: * User flags not supported.
334: *
335: * University of Washington IMAP server:
336: * "X-Status: DFAT", deleted, flagged, answered, draft.
337: * Unset flags not present.
338: * "X-Keywords: userflag1 userflag2"
339: */
340: private synchronized void setFlagsFromHeaders() {
341: flags = new Flags(Flags.Flag.RECENT);
342: try {
343: String s = getHeader("Status", null);
344: if (s != null) {
345: if (s.indexOf('R') >= 0)
346: flags.add(Flags.Flag.SEEN);
347: if (s.indexOf('O') >= 0)
348: flags.remove(Flags.Flag.RECENT);
349: }
350: s = getHeader("X-Dt-Delete-Time", null); // set by dtmail
351: if (s != null)
352: flags.add(Flags.Flag.DELETED);
353: s = getHeader("X-Status", null); // set by IMAP server
354: if (s != null) {
355: if (s.indexOf('D') >= 0)
356: flags.add(Flags.Flag.DELETED);
357: if (s.indexOf('F') >= 0)
358: flags.add(Flags.Flag.FLAGGED);
359: if (s.indexOf('A') >= 0)
360: flags.add(Flags.Flag.ANSWERED);
361: if (s.indexOf('T') >= 0)
362: flags.add(Flags.Flag.DRAFT);
363: }
364: s = getHeader("X-Keywords", null); // set by IMAP server
365: if (s != null) {
366: StringTokenizer st = new StringTokenizer(s);
367: while (st.hasMoreTokens())
368: flags.add(st.nextToken());
369: }
370: } catch (MessagingException e) {
371: // ignore it
372: }
373: }
374:
375: /**
376: * Set the various header fields that represent the message flags.
377: */
378: static void setHeadersFromFlags(MimeMessage msg) {
379: try {
380: Flags flags = msg.getFlags();
381: StringBuilder status = new StringBuilder();
382: if (flags.contains(Flags.Flag.SEEN))
383: status.append('R');
384: if (!flags.contains(Flags.Flag.RECENT))
385: status.append('O');
386: if (status.length() > 0)
387: msg.setHeader("Status", status.toString());
388: else
389: msg.removeHeader("Status");
390:
391: boolean sims = false;
392: String s = msg.getHeader("X-Status", null);
393: // is it a SIMS 2.0 format X-Status header?
394: sims = s != null && s.length() == 4 && s.indexOf('$') >= 0;
395: status.setLength(0);
396: if (flags.contains(Flags.Flag.DELETED))
397: status.append('D');
398: else if (sims)
399: status.append('$');
400: if (flags.contains(Flags.Flag.FLAGGED))
401: status.append('F');
402: else if (sims)
403: status.append('$');
404: if (flags.contains(Flags.Flag.ANSWERED))
405: status.append('A');
406: else if (sims)
407: status.append('$');
408: if (flags.contains(Flags.Flag.DRAFT))
409: status.append('T');
410: else if (sims)
411: status.append('$');
412: if (status.length() > 0)
413: msg.setHeader("X-Status", status.toString());
414: else
415: msg.removeHeader("X-Status");
416:
417: String[] userFlags = flags.getUserFlags();
418: if (userFlags.length > 0) {
419: status.setLength(0);
420: for (int i = 0; i < userFlags.length; i++)
421: status.append(userFlags[i]).append(' ');
422: status.setLength(status.length() - 1); // smash trailing space
423: msg.setHeader("X-Keywords", status.toString());
424: }
425: if (flags.contains(Flags.Flag.DELETED)) {
426: s = msg.getHeader("X-Dt-Delete-Time", null);
427: if (s == null)
428: // XXX - should be time
429: msg.setHeader("X-Dt-Delete-Time", "1");
430: }
431: } catch (MessagingException e) {
432: // ignore it
433: }
434: }
435:
436: protected void updateHeaders() throws MessagingException {
437: super.updateHeaders();
438: setHeadersFromFlags(this);
439: }
440:
441: /**
442: * Save any changes made to this message.
443: */
444: public void saveChanges() throws MessagingException {
445: if (folder != null)
446: ((MboxFolder) folder).checkOpen();
447: if (isExpunged())
448: throw new MessageRemovedException("mbox message expunged");
449: if (!writable)
450: throw new MessagingException("Message is read-only");
451:
452: super.saveChanges();
453:
454: try {
455: /*
456: * Count the size of the body, in order to set the Content-Length
457: * header. (Should we only do this to update an existing
458: * Content-Length header?)
459: * XXX - We could cache the content bytes here, for use later
460: * in writeTo.
461: */
462: ContentLengthCounter cos = new ContentLengthCounter();
463: OutputStream os = new NewlineOutputStream(cos);
464: super.writeTo(os);
465: os.flush();
466: setHeader("Content-Length", String.valueOf(cos.getSize()));
467: // setContentSize((int)cos.getSize());
468: } catch (MessagingException e) {
469: throw e;
470: } catch (Exception e) {
471: throw new MessagingException("unexpected exception " + e);
472: }
473: }
474:
475: /**
476: * Expose modified flag to MboxFolder.
477: */
478: boolean isModified() {
479: return modified;
480: }
481:
482: /**
483: * Put out a byte stream suitable for saving to a file.
484: * XXX - ultimately implement "ignore headers" here?
485: */
486: public void writeToFile(OutputStream os) throws IOException {
487: try {
488: if (getHeader("Content-Length") == null) {
489: /*
490: * Count the size of the body, in order to set the
491: * Content-Length header.
492: */
493: ContentLengthCounter cos = new ContentLengthCounter();
494: OutputStream oos = new NewlineOutputStream(cos);
495: super.writeTo(oos, null);
496: oos.flush();
497: setHeader("Content-Length", String.valueOf(cos.getSize()));
498: // setContentSize((int)cos.getSize());
499: }
500:
501: os = new NewlineOutputStream(os, true);
502: PrintStream pos = new PrintStream(os, false, "iso-8859-1");
503:
504: pos.println(getUnixFromLine());
505: super.writeTo(pos, null);
506: pos.flush();
507: } catch (MessagingException e) {
508: throw new IOException("unexpected exception " + e);
509: }
510: }
511:
512: public void writeTo(OutputStream os, String[] ignoreList)
513: throws IOException, MessagingException {
514: // set the SEEN flag now, which will normally be set by
515: // getContentStream, so it will show up in our headers
516: if (!isSet(Flags.Flag.SEEN))
517: setFlag(Flags.Flag.SEEN, true);
518: super.writeTo(os, ignoreList);
519: }
520:
521: /**
522: * Interpose on superclass method to make sure folder is still open
523: * and message hasn't been expunged.
524: */
525: public String[] getHeader(String name)
526: throws MessagingException {
527: if (folder != null)
528: ((MboxFolder) folder).checkOpen();
529: if (isExpunged())
530: throw new MessageRemovedException("mbox message expunged");
531: return super.getHeader(name);
532: }
533:
534: /**
535: * Interpose on superclass method to make sure folder is still open
536: * and message hasn't been expunged.
537: */
538: public String getHeader(String name, String delimiter)
539: throws MessagingException {
540: if (folder != null)
541: ((MboxFolder) folder).checkOpen();
542: if (isExpunged())
543: throw new MessageRemovedException("mbox message expunged");
544: return super.getHeader(name, delimiter);
545: }
546: }