001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common Development
008: * and Distribution License("CDDL") (collectively, the "License"). You
009: * may not use this file except in compliance with the License. You can obtain
010: * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
011: * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
012: * language governing permissions and limitations under the License.
013: *
014: * When distributing the software, include this License Header Notice in each
015: * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
016: * Sun designates this particular file as subject to the "Classpath" exception
017: * as provided by Sun in the GPL Version 2 section of the License file that
018: * accompanied this code. If applicable, add the following below the License
019: * Header, with the fields enclosed by brackets [] replaced by your own
020: * identifying information: "Portions Copyrighted [year]
021: * [name of copyright owner]"
022: *
023: * Contributor(s):
024: *
025: * If you wish your version of this file to be governed by only the CDDL or
026: * only the GPL Version 2, indicate your decision by adding "[Contributor]
027: * elects to include this software in this distribution under the [CDDL or GPL
028: * Version 2] license." If you don't indicate a single choice of license, a
029: * recipient has the option to distribute your version of this file under
030: * either the CDDL, the GPL Version 2 or to extend the choice of license to
031: * its licensees as provided above. However, if you add GPL Version 2 code
032: * and therefore, elected the GPL Version 2 license, then the option applies
033: * only if the new code is made subject to such option by the copyright
034: * holder.
035: */
036:
037: /*
038: * @(#)Protocol.java 1.27 07/05/04
039: */
040:
041: package com.sun.mail.pop3;
042:
043: import java.util.*;
044: import java.net.*;
045: import java.io.*;
046: import java.security.*;
047:
048: import com.sun.mail.util.LineInputStream;
049: import com.sun.mail.util.SocketFetcher;
050: import javax.mail.util.SharedByteArrayInputStream;
051:
052: class Response {
053: boolean ok = false; // true if "+OK"
054: String data = null; // rest of line after "+OK" or "-ERR"
055: InputStream bytes = null; // all the bytes from a multi-line response
056: }
057:
058: /**
059: * This class provides a POP3 connection and implements
060: * the POP3 protocol requests.
061: *
062: * APOP support courtesy of "chamness".
063: *
064: * @author Bill Shannon
065: */
066: class Protocol {
067: private Socket socket; // POP3 socket
068: private DataInputStream input; // input buf
069: private PrintWriter output; // output buf
070: private static final int POP3_PORT = 110; // standard POP3 port
071: private static final String CRLF = "\r\n";
072: private boolean debug = false;
073: private PrintStream out;
074: private String apopChallenge = null;
075:
076: /**
077: * Open a connection to the POP3 server.
078: */
079: Protocol(String host, int port, boolean debug, PrintStream out,
080: Properties props, String prefix, boolean isSSL)
081: throws IOException {
082: this .debug = debug;
083: this .out = out;
084: Response r;
085: String apop = props.getProperty(prefix + ".apop.enable");
086: boolean enableAPOP = apop != null
087: && apop.equalsIgnoreCase("true");
088: try {
089: if (port == -1)
090: port = POP3_PORT;
091: if (debug)
092: out.println("DEBUG POP3: connecting to host \"" + host
093: + "\", port " + port + ", isSSL " + isSSL);
094:
095: socket = SocketFetcher.getSocket(host, port, props, prefix,
096: isSSL);
097:
098: input = new DataInputStream(new BufferedInputStream(socket
099: .getInputStream()));
100: output = new PrintWriter(new BufferedWriter(
101: new OutputStreamWriter(socket.getOutputStream(),
102: "iso-8859-1")));
103: // should be US-ASCII, but not all JDK's support
104:
105: r = simpleCommand(null);
106: } catch (IOException ioe) {
107: try {
108: socket.close();
109: } finally {
110: throw ioe;
111: }
112: }
113:
114: if (!r.ok) {
115: try {
116: socket.close();
117: } finally {
118: throw new IOException("Connect failed");
119: }
120: }
121: if (enableAPOP) {
122: int challStart = r.data.indexOf('<'); // start of challenge
123: int challEnd = r.data.indexOf('>', challStart); // end of challenge
124: if (challStart != -1 && challEnd != -1)
125: apopChallenge = r.data.substring(challStart,
126: challEnd + 1);
127: if (debug)
128: out.println("DEBUG POP3: APOP challenge: "
129: + apopChallenge);
130: }
131: }
132:
133: protected void finalize() throws Throwable {
134: super .finalize();
135: if (socket != null) { // Forgot to logout ?!
136: quit();
137: }
138: }
139:
140: /**
141: * Login to the server, using the USER and PASS commands.
142: */
143: synchronized String login(String user, String password)
144: throws IOException {
145: Response r;
146: String dpw = null;
147: if (apopChallenge != null)
148: dpw = getDigest(password);
149: if (apopChallenge != null && dpw != null) {
150: r = simpleCommand("APOP " + user + " " + dpw);
151: } else {
152: r = simpleCommand("USER " + user);
153: if (!r.ok)
154: return r.data != null ? r.data : "USER command failed";
155: r = simpleCommand("PASS " + password);
156: }
157: if (!r.ok)
158: return r.data != null ? r.data : "login failed";
159: return null;
160: }
161:
162: /**
163: * Gets the APOP message digest.
164: * From RFC 1939:
165: *
166: * The 'digest' parameter is calculated by applying the MD5
167: * algorithm [RFC1321] to a string consisting of the timestamp
168: * (including angle-brackets) followed by a shared secret.
169: * The 'digest' parameter itself is a 16-octet value which is
170: * sent in hexadecimal format, using lower-case ASCII characters.
171: *
172: * @param password The APOP password
173: * @return The APOP digest or an empty string if an error occurs.
174: */
175: private String getDigest(String password) {
176: String key = apopChallenge + password;
177: byte[] digest;
178: try {
179: MessageDigest md = MessageDigest.getInstance("MD5");
180: digest = md.digest(key.getBytes("iso-8859-1")); // XXX
181: } catch (NoSuchAlgorithmException nsae) {
182: return null;
183: } catch (UnsupportedEncodingException uee) {
184: return null;
185: }
186: return toHex(digest);
187: }
188:
189: private static char[] digits = { '0', '1', '2', '3', '4', '5', '6',
190: '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
191:
192: /**
193: * Convert a byte array to a string of hex digits representing the bytes.
194: */
195: private static String toHex(byte[] bytes) {
196: char[] result = new char[bytes.length * 2];
197:
198: for (int index = 0, i = 0; index < bytes.length; index++) {
199: int temp = bytes[index] & 0xFF;
200: result[i++] = digits[temp >> 4];
201: result[i++] = digits[temp & 0xF];
202: }
203: return new String(result);
204: }
205:
206: /**
207: * Close down the connection, sending the QUIT command
208: * if expunge is true.
209: */
210: synchronized boolean quit() throws IOException {
211: boolean ok = false;
212: try {
213: Response r = simpleCommand("QUIT");
214: ok = r.ok;
215: } finally {
216: try {
217: socket.close();
218: } finally {
219: socket = null;
220: input = null;
221: output = null;
222: }
223: }
224: return ok;
225: }
226:
227: /**
228: * Return the total number of messages and mailbox size,
229: * using the STAT command.
230: */
231: synchronized Status stat() throws IOException {
232: Response r = simpleCommand("STAT");
233: Status s = new Status();
234: if (r.ok && r.data != null) {
235: try {
236: StringTokenizer st = new StringTokenizer(r.data);
237: s.total = Integer.parseInt(st.nextToken());
238: s.size = Integer.parseInt(st.nextToken());
239: } catch (Exception e) {
240: }
241: }
242: return s;
243: }
244:
245: /**
246: * Return the size of the message using the LIST command.
247: */
248: synchronized int list(int msg) throws IOException {
249: Response r = simpleCommand("LIST " + msg);
250: int size = -1;
251: if (r.ok && r.data != null) {
252: try {
253: StringTokenizer st = new StringTokenizer(r.data);
254: st.nextToken(); // skip message number
255: size = Integer.parseInt(st.nextToken());
256: } catch (Exception e) {
257: }
258: }
259: return size;
260: }
261:
262: /**
263: * Return the size of all messages using the LIST command.
264: */
265: synchronized InputStream list() throws IOException {
266: Response r = multilineCommand("LIST", 128); // 128 == output size est
267: return r.bytes;
268: }
269:
270: /**
271: * Retrieve the specified message.
272: * Given an estimate of the message's size we can be more efficient,
273: * preallocating the array and returning a SharedInputStream to allow
274: * us to share the array.
275: */
276: synchronized InputStream retr(int msg, int size) throws IOException {
277: Response r = multilineCommand("RETR " + msg, size);
278: return r.bytes;
279: }
280:
281: /**
282: * Return the message header and the first n lines of the message.
283: */
284: synchronized InputStream top(int msg, int n) throws IOException {
285: Response r = multilineCommand("TOP " + msg + " " + n, 0);
286: return r.bytes;
287: }
288:
289: /**
290: * Delete (permanently) the specified message.
291: */
292: synchronized boolean dele(int msg) throws IOException {
293: Response r = simpleCommand("DELE " + msg);
294: return r.ok;
295: }
296:
297: /**
298: * Return the UIDL string for the message.
299: */
300: synchronized String uidl(int msg) throws IOException {
301: Response r = simpleCommand("UIDL " + msg);
302: if (!r.ok)
303: return null;
304: int i = r.data.indexOf(' ');
305: if (i > 0)
306: return r.data.substring(i + 1);
307: else
308: return null;
309: }
310:
311: /**
312: * Return the UIDL strings for all messages.
313: * The UID for msg #N is returned in uids[N-1].
314: */
315: synchronized boolean uidl(String[] uids) throws IOException {
316: Response r = multilineCommand("UIDL", 15 * uids.length);
317: if (!r.ok)
318: return false;
319: LineInputStream lis = new LineInputStream(r.bytes);
320: String line = null;
321: while ((line = lis.readLine()) != null) {
322: int i = line.indexOf(' ');
323: if (i < 1 || i >= line.length())
324: continue;
325: int n = Integer.parseInt(line.substring(0, i));
326: if (n > 0 && n <= uids.length)
327: uids[n - 1] = line.substring(i + 1);
328: }
329: return true;
330: }
331:
332: /**
333: * Do a NOOP.
334: */
335: synchronized boolean noop() throws IOException {
336: Response r = simpleCommand("NOOP");
337: return r.ok;
338: }
339:
340: /**
341: * Do an RSET.
342: */
343: synchronized boolean rset() throws IOException {
344: Response r = simpleCommand("RSET");
345: return r.ok;
346: }
347:
348: /**
349: * Issue a simple POP3 command and return the response.
350: */
351: private Response simpleCommand(String cmd) throws IOException {
352: if (socket == null)
353: throw new IOException("Folder is closed"); // XXX
354: if (cmd != null) {
355: if (debug)
356: out.println("C: " + cmd);
357: cmd += CRLF;
358: output.print(cmd); // do it in one write
359: output.flush();
360: }
361: String line = input.readLine(); // XXX - readLine is deprecated
362: if (line == null) {
363: if (debug)
364: out.println("S: EOF");
365: throw new EOFException("EOF on socket");
366: }
367: if (debug)
368: out.println("S: " + line);
369: Response r = new Response();
370: if (line.startsWith("+OK"))
371: r.ok = true;
372: else if (line.startsWith("-ERR"))
373: r.ok = false;
374: else
375: throw new IOException("Unexpected response: " + line);
376: int i;
377: if ((i = line.indexOf(' ')) >= 0)
378: r.data = line.substring(i + 1);
379: return r;
380: }
381:
382: /**
383: * Issue a POP3 command that expects a multi-line response.
384: * <code>size</code> is an estimate of the response size.
385: */
386: private Response multilineCommand(String cmd, int size)
387: throws IOException {
388: Response r = simpleCommand(cmd);
389: if (!r.ok)
390: return (r);
391:
392: SharedByteArrayOutputStream buf = new SharedByteArrayOutputStream(
393: size);
394: int b, lastb = '\n';
395: while ((b = input.read()) >= 0) {
396: if (lastb == '\n' && b == '.') {
397: if (debug)
398: out.write(b);
399: b = input.read();
400: if (b == '\r') {
401: if (debug)
402: out.write(b);
403: // end of response, consume LF as well
404: b = input.read();
405: if (debug)
406: out.write(b);
407: break;
408: }
409: }
410: buf.write(b);
411: if (debug)
412: out.write(b);
413: lastb = b;
414: }
415: if (b < 0)
416: throw new EOFException("EOF on socket");
417: r.bytes = buf.toStream();
418: return r;
419: }
420: }
421:
422: /**
423: * A ByteArrayOutputStream that allows us to share the byte array
424: * rather than copy it. Eventually could replace this with something
425: * that doesn't require a single contiguous byte array.
426: */
427: class SharedByteArrayOutputStream extends ByteArrayOutputStream {
428: public SharedByteArrayOutputStream(int size) {
429: super (size);
430: }
431:
432: public InputStream toStream() {
433: return new SharedByteArrayInputStream(buf, 0, count);
434: }
435: }
|