Skip to content

Package: Response

Response

nameinstructionbranchcomplexitylinemethod
Response()
M: 15 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 5 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.pop3;
18:
19: import org.eclipse.angus.mail.auth.Ntlm;
20: import org.eclipse.angus.mail.util.ASCIIUtility;
21: import org.eclipse.angus.mail.util.BASE64EncoderStream;
22: import org.eclipse.angus.mail.util.LineInputStream;
23: import org.eclipse.angus.mail.util.MailLogger;
24: import org.eclipse.angus.mail.util.PropUtil;
25: import org.eclipse.angus.mail.util.SharedByteArrayOutputStream;
26: import org.eclipse.angus.mail.util.SocketFetcher;
27: import org.eclipse.angus.mail.util.TraceInputStream;
28: import org.eclipse.angus.mail.util.TraceOutputStream;
29:
30: import javax.net.ssl.SSLSocket;
31: import java.io.BufferedReader;
32: import java.io.BufferedWriter;
33: import java.io.ByteArrayOutputStream;
34: import java.io.EOFException;
35: import java.io.IOException;
36: import java.io.InputStream;
37: import java.io.InputStreamReader;
38: import java.io.InterruptedIOException;
39: import java.io.OutputStream;
40: import java.io.OutputStreamWriter;
41: import java.io.PrintWriter;
42: import java.net.InetAddress;
43: import java.net.Socket;
44: import java.net.SocketException;
45: import java.net.UnknownHostException;
46: import java.nio.charset.StandardCharsets;
47: import java.security.MessageDigest;
48: import java.security.NoSuchAlgorithmException;
49: import java.util.Base64;
50: import java.util.HashMap;
51: import java.util.Locale;
52: import java.util.Map;
53: import java.util.Properties;
54: import java.util.StringTokenizer;
55: import java.util.logging.Level;
56:
57: class Response {
58: boolean ok = false; // true if "+OK"
59: boolean cont = false; // true if "+ " continuation line
60: String data = null; // rest of line after "+OK" or "-ERR"
61: InputStream bytes = null; // all the bytes from a multi-line response
62: }
63:
64: /**
65: * This class provides a POP3 connection and implements
66: * the POP3 protocol requests.
67: *
68: * APOP support courtesy of "chamness".
69: *
70: * @author Bill Shannon
71: */
72: class Protocol {
73: private Socket socket; // POP3 socket
74: private String host; // host we're connected to
75: private Properties props; // session properties
76: private String prefix; // protocol name prefix, for props
77: private BufferedReader input; // input buf
78: private PrintWriter output; // output buf
79: private TraceInputStream traceInput;
80: private TraceOutputStream traceOutput;
81: private MailLogger logger;
82: private MailLogger traceLogger;
83: private String apopChallenge = null;
84: private Map<String, String> capabilities = null;
85: private boolean pipelining;
86: private boolean noauthdebug = true; // hide auth info in debug output
87: private boolean traceSuspended; // temporarily suspend tracing
88: private Map<String, Authenticator> authenticators = new HashMap<>();
89: private String defaultAuthenticationMechanisms; // set in constructor
90: private String localHostName;
91:
92: private static final int POP3_PORT = 110; // standard POP3 port
93: private static final String CRLF = "\r\n";
94: // sometimes the returned size isn't quite big enough
95: private static final int SLOP = 128;
96:
97: /**
98: * Open a connection to the POP3 server.
99: */
100: Protocol(String host, int port, MailLogger logger,
101: Properties props, String prefix, boolean isSSL)
102: throws IOException {
103: this.host = host;
104: this.props = props;
105: this.prefix = prefix;
106: this.logger = logger;
107: traceLogger = logger.getSubLogger("protocol", null);
108: noauthdebug = !PropUtil.getBooleanProperty(props,
109: "mail.debug.auth", false);
110:
111: Response r;
112: boolean enableAPOP = getBoolProp(props, prefix + ".apop.enable");
113: boolean disableCapa = getBoolProp(props, prefix + ".disablecapa");
114: try {
115: if (port == -1)
116: port = POP3_PORT;
117: if (logger.isLoggable(Level.FINE))
118: logger.fine("connecting to host \"" + host +
119: "\", port " + port + ", isSSL " + isSSL);
120:
121: socket = SocketFetcher.getSocket(host, port, props, prefix, isSSL);
122: initStreams();
123: r = simpleCommand(null);
124: } catch (IOException ioe) {
125: throw cleanupAndThrow(socket, ioe);
126: }
127:
128: if (!r.ok) {
129: throw cleanupAndThrow(socket, new IOException("Connect failed"));
130: }
131: if (enableAPOP && r.data != null) {
132: int challStart = r.data.indexOf('<'); // start of challenge
133: int challEnd = r.data.indexOf('>', challStart); // end of challenge
134: if (challStart != -1 && challEnd != -1)
135: apopChallenge = r.data.substring(challStart, challEnd + 1);
136: logger.log(Level.FINE, "APOP challenge: {0}", apopChallenge);
137: }
138:
139: // if server supports RFC 2449, set capabilities
140: if (!disableCapa)
141: setCapabilities(capa());
142:
143: pipelining = hasCapability("PIPELINING") ||
144: PropUtil.getBooleanProperty(props, prefix + ".pipelining", false);
145: if (pipelining)
146: logger.config("PIPELINING enabled");
147:
148: // created here, because they're inner classes that reference "this"
149: Authenticator[] a = new Authenticator[]{
150: new LoginAuthenticator(),
151: new PlainAuthenticator(),
152: //new DigestMD5Authenticator(),
153: new NtlmAuthenticator(),
154: new OAuth2Authenticator()
155: };
156: StringBuilder sb = new StringBuilder();
157: for (int i = 0; i < a.length; i++) {
158: authenticators.put(a[i].getMechanism(), a[i]);
159: sb.append(a[i].getMechanism()).append(' ');
160: }
161: defaultAuthenticationMechanisms = sb.toString();
162: }
163:
164: private static IOException cleanupAndThrow(Socket socket, IOException ife) {
165: if (socket != null) {
166: try {
167: socket.close();
168: } catch (Throwable thr) {
169: if (isRecoverable(thr)) {
170: ife.addSuppressed(thr);
171: } else {
172: thr.addSuppressed(ife);
173: if (thr instanceof Error) {
174: throw (Error) thr;
175: }
176: if (thr instanceof RuntimeException) {
177: throw (RuntimeException) thr;
178: }
179: throw new RuntimeException("unexpected exception", thr);
180: }
181: }
182: }
183: return ife;
184: }
185:
186: private static boolean isRecoverable(Throwable t) {
187: return (t instanceof Exception) || (t instanceof LinkageError);
188: }
189:
190: /**
191: * Get the value of a boolean property.
192: * Print out the value if logging is enabled.
193: */
194: private final synchronized boolean getBoolProp(Properties props,
195: String prop) {
196: boolean val = PropUtil.getBooleanProperty(props, prop, false);
197: if (logger.isLoggable(Level.CONFIG))
198: logger.config(prop + ": " + val);
199: return val;
200: }
201:
202: private void initStreams() throws IOException {
203: boolean quote = PropUtil.getBooleanProperty(props,
204: "mail.debug.quote", false);
205: traceInput =
206: new TraceInputStream(socket.getInputStream(), traceLogger);
207: traceInput.setQuote(quote);
208:
209: traceOutput =
210: new TraceOutputStream(socket.getOutputStream(), traceLogger);
211: traceOutput.setQuote(quote);
212:
213: // should be US-ASCII, but not all JDK's support it so use iso-8859-1
214: input = new BufferedReader(new InputStreamReader(traceInput,
215: StandardCharsets.ISO_8859_1));
216: output = new PrintWriter(
217: new BufferedWriter(
218: new OutputStreamWriter(traceOutput, StandardCharsets.ISO_8859_1)));
219: }
220:
221: @Override
222: protected void finalize() throws Throwable {
223: try {
224: if (socket != null) // Forgot to logout ?!
225: quit();
226: } finally {
227: super.finalize();
228: }
229: }
230:
231: /**
232: * Parse the capabilities from a CAPA response.
233: */
234: synchronized void setCapabilities(InputStream in) {
235: if (in == null) {
236: capabilities = null;
237: return;
238: }
239:
240: capabilities = new HashMap<>(10);
241: BufferedReader r = null;
242: r = new BufferedReader(new InputStreamReader(in, StandardCharsets.US_ASCII));
243: String s;
244: try {
245: while ((s = r.readLine()) != null) {
246: String cap = s;
247: int i = cap.indexOf(' ');
248: if (i > 0)
249: cap = cap.substring(0, i);
250: capabilities.put(cap.toUpperCase(Locale.ENGLISH), s);
251: }
252: } catch (IOException ex) {
253: // should never happen
254: } finally {
255: try {
256: in.close();
257: } catch (IOException ex) {
258: }
259: }
260: }
261:
262: /**
263: * Check whether the given capability is supported by
264: * this server. Returns <code>true</code> if so, otherwise
265: * returns false.
266: */
267: synchronized boolean hasCapability(String c) {
268: return capabilities != null &&
269: capabilities.containsKey(c.toUpperCase(Locale.ENGLISH));
270: }
271:
272: /**
273: * Return the map of capabilities returned by the server.
274: */
275: synchronized Map<String, String> getCapabilities() {
276: return capabilities;
277: }
278:
279: /**
280: * Does this Protocol object support the named authentication mechanism?
281: *
282: * @since Jakarta Mail 1.6.5
283: */
284: boolean supportsMechanism(String mech) {
285: return authenticators.containsKey(mech.toUpperCase(Locale.ENGLISH));
286: }
287:
288: /**
289: * Return the whitespace separated string list of default authentication
290: * mechanisms.
291: *
292: * @since Jakarta Mail 1.6.5
293: */
294: String getDefaultMechanisms() {
295: return defaultAuthenticationMechanisms;
296: }
297:
298: /**
299: * Is the named authentication mechanism enabled?
300: *
301: * @since Jakarta Mail 1.6.5
302: */
303: boolean isMechanismEnabled(String mech) {
304: Authenticator a = authenticators.get(mech.toUpperCase(Locale.ENGLISH));
305: return a != null && a.enabled();
306: }
307:
308: /**
309: * Authenticate to the server using the named authentication mechanism
310: * and the supplied credentials.
311: *
312: * @since Jakarta Mail 1.6.5
313: */
314: synchronized String authenticate(String mech,
315: String host, String authzid,
316: String user, String passwd) {
317: Authenticator a = authenticators.get(mech.toUpperCase(Locale.ENGLISH));
318: if (a == null)
319: return "No such authentication mechanism: " + mech;
320: try {
321: if (!a.authenticate(host, authzid, user, passwd))
322: return "login failed";
323: return null;
324: } catch (IOException ex) {
325: return ex.getMessage();
326: }
327: }
328:
329: /**
330: * Does the server we're connected to support the specified
331: * authentication mechanism? Uses the information
332: * returned by the server from the CAPA command.
333: *
334: * @param auth the authentication mechanism
335: * @return true if the authentication mechanism is supported
336: * @since Jakarta Mail 1.6.5
337: */
338: synchronized boolean supportsAuthentication(String auth) {
339: assert Thread.holdsLock(this);
340: if (auth.equals("LOGIN"))
341: return true;
342: if (capabilities == null)
343: return false;
344: String a = capabilities.get("SASL");
345: if (a == null)
346: return false;
347: StringTokenizer st = new StringTokenizer(a);
348: while (st.hasMoreTokens()) {
349: String tok = st.nextToken();
350: if (tok.equalsIgnoreCase(auth))
351: return true;
352: }
353: return false;
354: }
355:
356: /**
357: * Login to the server, using the USER and PASS commands.
358: */
359: synchronized String login(String user, String password)
360: throws IOException {
361: Response r;
362: // only pipeline password if connection is secure
363: boolean batch = pipelining && socket instanceof SSLSocket;
364:
365: try {
366:
367: if (noauthdebug && isTracing()) {
368: logger.fine("authentication command trace suppressed");
369: suspendTracing();
370: }
371: String dpw = null;
372: if (apopChallenge != null)
373: dpw = getDigest(password);
374: if (apopChallenge != null && dpw != null) {
375: r = simpleCommand("APOP " + user + " " + dpw);
376: } else if (batch) {
377: String cmd = "USER " + user;
378: batchCommandStart(cmd);
379: issueCommand(cmd);
380: cmd = "PASS " + password;
381: batchCommandContinue(cmd);
382: issueCommand(cmd);
383: r = readResponse();
384: if (!r.ok) {
385: String err = r.data != null ? r.data : "USER command failed";
386: readResponse(); // read and ignore PASS response
387: batchCommandEnd();
388: return err;
389: }
390: r = readResponse();
391: batchCommandEnd();
392: } else {
393: r = simpleCommand("USER " + user);
394: if (!r.ok)
395: return r.data != null ? r.data : "USER command failed";
396: r = simpleCommand("PASS " + password);
397: }
398: if (noauthdebug && isTracing())
399: logger.log(Level.FINE, "authentication command {0}",
400: (r.ok ? "succeeded" : "failed"));
401: if (!r.ok)
402: return r.data != null ? r.data : "login failed";
403: return null;
404:
405: } finally {
406: resumeTracing();
407: }
408: }
409:
410: /**
411: * Gets the APOP message digest.
412: * From RFC 1939:
413: *
414: * The 'digest' parameter is calculated by applying the MD5
415: * algorithm [RFC1321] to a string consisting of the timestamp
416: * (including angle-brackets) followed by a shared secret.
417: * The 'digest' parameter itself is a 16-octet value which is
418: * sent in hexadecimal format, using lower-case ASCII characters.
419: *
420: * @param password The APOP password
421: * @return The APOP digest or an empty string if an error occurs.
422: */
423: private String getDigest(String password) {
424: String key = apopChallenge + password;
425: byte[] digest;
426: try {
427: MessageDigest md = MessageDigest.getInstance("MD5");
428: digest = md.digest(key.getBytes(StandardCharsets.ISO_8859_1)); // XXX
429: } catch (NoSuchAlgorithmException nsae) {
430: return null;
431: }
432: return toHex(digest);
433: }
434:
435: /**
436: * Abstract base class for POP3 authentication mechanism implementations.
437: *
438: * @since Jakarta Mail 1.6.5
439: */
440: private abstract class Authenticator {
441: protected Response resp; // the response, used by subclasses
442: private final String mech; // the mechanism name, set in the constructor
443: private final boolean enabled; // is this mechanism enabled by default?
444:
445: Authenticator(String mech) {
446: this(mech, true);
447: }
448:
449: Authenticator(String mech, boolean enabled) {
450: this.mech = mech.toUpperCase(Locale.ENGLISH);
451: this.enabled = enabled;
452: }
453:
454: String getMechanism() {
455: return mech;
456: }
457:
458: boolean enabled() {
459: return enabled;
460: }
461:
462: /**
463: * Run authentication query based on command and initial response
464: *
465: * @param command - command passed to server
466: * @param ir - initial response, part of the query
467: */
468: protected void runAuthenticationCommand(String command, String ir) throws IOException {
469: if (logger.isLoggable(Level.FINE)) {
470: logger.fine(command + " using one line authentication format");
471: }
472:
473: if (ir != null) {
474: resp = simpleCommand(command + " " + (ir.length() == 0 ? "=" : ir));
475: } else {
476: resp = simpleCommand(command);
477: }
478: }
479:
480: /**
481: * Start the authentication handshake by issuing the AUTH command.
482: * Delegate to the doAuth method to do the mechanism-specific
483: * part of the handshake.
484: */
485: boolean authenticate(String host, String authzid,
486: String user, String passwd) throws IOException {
487: Throwable thrown = null;
488: try {
489: // use "initial response" capability, if supported
490: String ir = getInitialResponse(host, authzid, user, passwd);
491: if (noauthdebug && isTracing()) {
492: logger.fine("AUTH " + mech + " command trace suppressed");
493: suspendTracing();
494: }
495:
496: runAuthenticationCommand("AUTH " + mech, ir);
497:
498: if (resp.cont)
499: doAuth(host, authzid, user, passwd);
500: } catch (IOException ex) { // should never happen, ignore
501: logger.log(Level.FINE, "AUTH " + mech + " failed", ex);
502: } catch (Throwable t) { // crypto can't be initialized?
503: logger.log(Level.FINE, "AUTH " + mech + " failed", t);
504: thrown = t;
505: } finally {
506: if (noauthdebug && isTracing())
507: logger.fine("AUTH " + mech + " " +
508: (resp.ok ? "succeeded" : "failed"));
509: resumeTracing();
510: if (!resp.ok) {
511: close();
512: if (thrown != null) {
513: if (thrown instanceof Error)
514: throw (Error) thrown;
515: if (thrown instanceof Exception) {
516: EOFException ex = new EOFException(
517: resp.data != null ?
518: resp.data : "authentication failed");
519: ex.initCause(thrown);
520: throw ex;
521: }
522: assert false : "unknown Throwable"; // can't happen
523: }
524: throw new EOFException(resp.data != null ?
525: resp.data : "authentication failed");
526: }
527: }
528: return true;
529: }
530:
531: /**
532: * Provide the initial response to use in the AUTH command,
533: * or null if not supported. Subclasses that support the
534: * initial response capability will override this method.
535: */
536: String getInitialResponse(String host, String authzid, String user,
537: String passwd) throws IOException {
538: return null;
539: }
540:
541: abstract void doAuth(String host, String authzid, String user,
542: String passwd) throws IOException;
543: }
544:
545: /**
546: * Perform the authentication handshake for LOGIN authentication.
547: *
548: * @since Jakarta Mail 1.6.5
549: */
550: private class LoginAuthenticator extends Authenticator {
551: LoginAuthenticator() {
552: super("LOGIN");
553: }
554:
555: @Override
556: boolean authenticate(String host, String authzid,
557: String user, String passwd) throws IOException {
558: String msg = null;
559: if ((msg = login(user, passwd)) != null) {
560: throw new EOFException(msg);
561: }
562: return true;
563: }
564:
565: @Override
566: void doAuth(String host, String authzid, String user, String passwd)
567: throws IOException {
568: // should never get here
569: throw new EOFException("LOGIN asked for more");
570: }
571: }
572:
573: /**
574: * Perform the authentication handshake for PLAIN authentication.
575: *
576: * @since Jakarta Mail 1.6.5
577: */
578: private class PlainAuthenticator extends Authenticator {
579: PlainAuthenticator() {
580: super("PLAIN");
581: }
582:
583: @Override
584: String getInitialResponse(String host, String authzid, String user,
585: String passwd) throws IOException {
586: // return "authzid<NUL>user<NUL>passwd"
587: ByteArrayOutputStream bos = new ByteArrayOutputStream();
588: OutputStream b64os =
589: new BASE64EncoderStream(bos, Integer.MAX_VALUE);
590: if (authzid != null)
591: b64os.write(authzid.getBytes(StandardCharsets.UTF_8));
592: b64os.write(0);
593: b64os.write(user.getBytes(StandardCharsets.UTF_8));
594: b64os.write(0);
595: b64os.write(passwd.getBytes(StandardCharsets.UTF_8));
596: b64os.flush(); // complete the encoding
597:
598: return ASCIIUtility.toString(bos.toByteArray());
599: }
600:
601: @Override
602: void doAuth(String host, String authzid, String user, String passwd)
603: throws IOException {
604: // should never get here
605: throw new EOFException("PLAIN asked for more");
606: }
607: }
608:
609: /**
610: * Perform the authentication handshake for DIGEST-MD5 authentication.
611: *
612: * @since Jakarta Mail 1.6.5
613: */
614: /*
615: * XXX - Need to move DigestMD5 class to org.eclipse.angus.mail.auth
616: *
617: private class DigestMD5Authenticator extends Authenticator {
618:         private DigestMD5 md5support;        // only create if needed
619:
620:         DigestMD5Authenticator() {
621:          super("DIGEST-MD5");
622:         }
623:
624:         private synchronized DigestMD5 getMD5() {
625:          if (md5support == null)
626:                 md5support = new DigestMD5(logger);
627:          return md5support;
628:         }
629:
630:         @Override
631:         void doAuth(Protocol p, String host, String authzid,
632:                                         String user, String passwd)
633:                                  throws IOException {
634:          DigestMD5 md5 = getMD5();
635:          assert md5 != null;
636:
637:          byte[] b = md5.authClient(host, user, passwd, getSASLRealm(),
638:                                         resp.data);
639:          resp = p.simpleCommand(b);
640:          if (resp.cont) { // client authenticated by server
641:                 if (!md5.authServer(resp.data)) {
642:                  // server NOT authenticated by client !!!
643:                  resp.ok = false;
644:                 } else {
645:                  // send null response
646:                  resp = simpleCommand(new byte[0]);
647:                 }
648:          }
649:         }
650: }
651: */
652:
653: /**
654: * Perform the authentication handshake for NTLM authentication.
655: *
656: * @since Jakarta Mail 1.6.5
657: */
658: private class NtlmAuthenticator extends Authenticator {
659: private Ntlm ntlm;
660:
661: NtlmAuthenticator() {
662: super("NTLM");
663: }
664:
665: @Override
666: String getInitialResponse(String host, String authzid, String user,
667: String passwd) throws IOException {
668: ntlm = new Ntlm(props.getProperty(prefix + ".auth.ntlm.domain"),
669: getLocalHost(), user, passwd, logger);
670:
671: int flags = PropUtil.getIntProperty(
672: props, prefix + ".auth.ntlm.flags", 0);
673: boolean v2 = PropUtil.getBooleanProperty(
674: props, prefix + ".auth.ntlm.v2", true);
675:
676: String type1 = ntlm.generateType1Msg(flags, v2);
677: return type1;
678: }
679:
680: @Override
681: void doAuth(String host, String authzid, String user, String passwd)
682: throws IOException {
683: assert ntlm != null;
684: String type3 = ntlm.generateType3Msg(
685: resp.data.substring(4).trim());
686:
687: resp = simpleCommand(type3);
688: }
689: }
690:
691: /**
692: * Perform the authentication handshake for XOAUTH2 authentication.
693: *
694: * @since Jakarta Mail 1.6.5
695: */
696: private class OAuth2Authenticator extends Authenticator {
697:
698: OAuth2Authenticator() {
699: super("XOAUTH2", false); // disabled by default
700: }
701:
702: @Override
703: String getInitialResponse(String host, String authzid, String user,
704: String passwd) throws IOException {
705: String resp = "user=" + user + "\001auth=Bearer " +
706: passwd + "\001\001";
707: byte[] b = Base64.getEncoder().encode(
708: resp.getBytes(StandardCharsets.UTF_8));
709: return ASCIIUtility.toString(b);
710: }
711:
712: @Override
713: protected void runAuthenticationCommand(String command, String ir) throws IOException {
714: Boolean isTwoLineAuthenticationFormat = getBoolProp(
715: props,
716: prefix + ".auth.xoauth2.two.line.authentication.format");
717:
718: if (isTwoLineAuthenticationFormat) {
719: if (logger.isLoggable(Level.FINE)) {
720: logger.fine(command + " using two line authentication format");
721: }
722:
723: resp = twoLinesCommand(
724: command,
725: (ir.length() == 0 ? "=" : ir)
726: );
727: } else {
728: super.runAuthenticationCommand(command, ir);
729: }
730: }
731:
732: @Override
733: void doAuth(String host, String authzid, String user, String passwd)
734: throws IOException {
735: // OAuth2 failure returns a JSON error code,
736: // which looks like a "please continue" to the authenticate()
737: // code, so we turn that into a clean failure here.
738: String err = "";
739: if (resp.data != null) {
740: byte[] b = resp.data.getBytes(StandardCharsets.UTF_8);
741: b = Base64.getDecoder().decode(b);
742: err = new String(b, StandardCharsets.UTF_8);
743: }
744: throw new EOFException("OAUTH2 authentication failed: " + err);
745: }
746: }
747:
748: /**
749: * Get the name of the local host.
750: *
751: * @return the local host name
752: * @since Jakarta Mail 1.6.5
753: */
754: private synchronized String getLocalHost() {
755: // get our hostname and cache it for future use
756: try {
757: if (localHostName == null || localHostName.length() == 0) {
758: InetAddress localHost = InetAddress.getLocalHost();
759: localHostName = localHost.getCanonicalHostName();
760: // if we can't get our name, use local address literal
761: if (localHostName == null)
762: // XXX - not correct for IPv6
763: localHostName = "[" + localHost.getHostAddress() + "]";
764: }
765: } catch (UnknownHostException uhex) {
766: }
767:
768: // last chance, try to get our address from our socket
769: if (localHostName == null || localHostName.length() <= 0) {
770: if (socket != null && socket.isBound()) {
771: InetAddress localHost = socket.getLocalAddress();
772: localHostName = localHost.getCanonicalHostName();
773: // if we can't get our name, use local address literal
774: if (localHostName == null)
775: // XXX - not correct for IPv6
776: localHostName = "[" + localHost.getHostAddress() + "]";
777: }
778: }
779: return localHostName;
780: }
781:
782: private static char[] digits = {
783: '0', '1', '2', '3', '4', '5', '6', '7',
784: '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
785: };
786:
787: /**
788: * Convert a byte array to a string of hex digits representing the bytes.
789: */
790: private static String toHex(byte[] bytes) {
791: char[] result = new char[bytes.length * 2];
792:
793: for (int index = 0, i = 0; index < bytes.length; index++) {
794: int temp = bytes[index] & 0xFF;
795: result[i++] = digits[temp >> 4];
796: result[i++] = digits[temp & 0xF];
797: }
798: return new String(result);
799: }
800:
801: /**
802: * Close down the connection, sending the QUIT command.
803: */
804: synchronized boolean quit() throws IOException {
805: boolean ok = false;
806: try {
807: Response r = simpleCommand("QUIT");
808: ok = r.ok;
809: } finally {
810: close();
811: }
812: return ok;
813: }
814:
815: /**
816: * Close the connection without sending any commands.
817: */
818: void close() {
819: try {
820: if (socket != null)
821: socket.close();
822: } catch (IOException ex) {
823: // ignore it
824: } finally {
825: socket = null;
826: input = null;
827: output = null;
828: }
829: }
830:
831: /**
832: * Return the total number of messages and mailbox size,
833: * using the STAT command.
834: */
835: synchronized Status stat() throws IOException {
836: Response r = simpleCommand("STAT");
837: Status s = new Status();
838:
839: /*
840: * Normally the STAT command shouldn't fail but apparently it
841: * does when accessing Hotmail too often, returning:
842: * -ERR login allowed only every 15 minutes
843: * (Why it doesn't just fail the login, I don't know.)
844: * This is a serious failure that we don't want to hide
845: * from the user.
846: */
847: if (!r.ok)
848: throw new IOException("STAT command failed: " + r.data);
849:
850: if (r.data != null) {
851: try {
852: StringTokenizer st = new StringTokenizer(r.data);
853: s.total = Integer.parseInt(st.nextToken());
854: s.size = Integer.parseInt(st.nextToken());
855: } catch (RuntimeException e) {
856: }
857: }
858: return s;
859: }
860:
861: /**
862: * Return the size of the message using the LIST command.
863: */
864: synchronized int list(int msg) throws IOException {
865: Response r = simpleCommand("LIST " + msg);
866: int size = -1;
867: if (r.ok && r.data != null) {
868: try {
869: StringTokenizer st = new StringTokenizer(r.data);
870: st.nextToken(); // skip message number
871: size = Integer.parseInt(st.nextToken());
872: } catch (RuntimeException e) {
873: // ignore it
874: }
875: }
876: return size;
877: }
878:
879: /**
880: * Return the size of all messages using the LIST command.
881: */
882: synchronized InputStream list() throws IOException {
883: Response r = multilineCommand("LIST", 128); // 128 == output size est
884: return r.bytes;
885: }
886:
887: /**
888: * Retrieve the specified message.
889: * Given an estimate of the message's size we can be more efficient,
890: * preallocating the array and returning a SharedInputStream to allow
891: * us to share the array.
892: */
893: synchronized InputStream retr(int msg, int size) throws IOException {
894: Response r;
895: String cmd;
896: boolean batch = size == 0 && pipelining;
897: if (batch) {
898: cmd = "LIST " + msg;
899: batchCommandStart(cmd);
900: issueCommand(cmd);
901: cmd = "RETR " + msg;
902: batchCommandContinue(cmd);
903: issueCommand(cmd);
904: r = readResponse();
905: if (r.ok && r.data != null) {
906: // parse the LIST response to get the message size
907: try {
908: StringTokenizer st = new StringTokenizer(r.data);
909: st.nextToken(); // skip message number
910: size = Integer.parseInt(st.nextToken());
911: // don't allow ridiculous sizes
912: if (size > 1024 * 1024 * 1024 || size < 0)
913: size = 0;
914: else {
915: if (logger.isLoggable(Level.FINE))
916: logger.fine("pipeline message size " + size);
917: size += SLOP;
918: }
919: } catch (RuntimeException e) {
920: }
921: }
922: r = readResponse();
923: if (r.ok)
924: r.bytes = readMultilineResponse(size + SLOP);
925: batchCommandEnd();
926: } else {
927: cmd = "RETR " + msg;
928: multilineCommandStart(cmd);
929: issueCommand(cmd);
930: r = readResponse();
931: if (!r.ok) {
932: multilineCommandEnd();
933: return null;
934: }
935:
936: /*
937: * Many servers return a response to the RETR command of the form:
938: * +OK 832 octets
939: * If we don't have a size guess already, try to parse the response
940: * for data in that format and use it if found. It's only a guess,
941: * but it might be a good guess.
942: */
943: if (size <= 0 && r.data != null) {
944: try {
945: StringTokenizer st = new StringTokenizer(r.data);
946: String s = st.nextToken();
947: String octets = st.nextToken();
948: if (octets.equals("octets")) {
949: size = Integer.parseInt(s);
950: // don't allow ridiculous sizes
951: if (size > 1024 * 1024 * 1024 || size < 0)
952: size = 0;
953: else {
954: if (logger.isLoggable(Level.FINE))
955: logger.fine("guessing message size: " + size);
956: size += SLOP;
957: }
958: }
959: } catch (RuntimeException e) {
960: }
961: }
962: r.bytes = readMultilineResponse(size);
963: multilineCommandEnd();
964: }
965: if (r.ok) {
966: if (size > 0 && logger.isLoggable(Level.FINE))
967: logger.fine("got message size " + r.bytes.available());
968: }
969: return r.bytes;
970: }
971:
972: /**
973: * Retrieve the specified message and stream the content to the
974: * specified OutputStream. Return true on success.
975: */
976: synchronized boolean retr(int msg, OutputStream os) throws IOException {
977: String cmd = "RETR " + msg;
978: multilineCommandStart(cmd);
979: issueCommand(cmd);
980: Response r = readResponse();
981: if (!r.ok) {
982: multilineCommandEnd();
983: return false;
984: }
985:
986: Throwable terr = null;
987: int b, lastb = '\n';
988: try {
989: while ((b = input.read()) >= 0) {
990: if (lastb == '\n' && b == '.') {
991: b = input.read();
992: if (b == '\r') {
993: // end of response, consume LF as well
994: b = input.read();
995: break;
996: }
997: }
998:
999: /*
1000: * Keep writing unless we get an error while writing,
1001: * which we defer until all of the data has been read.
1002: */
1003: if (terr == null) {
1004: try {
1005: os.write(b);
1006: } catch (IOException | RuntimeException ex) {
1007: logger.log(Level.FINE, "exception while streaming", ex);
1008: terr = ex;
1009: }
1010: }
1011: lastb = b;
1012: }
1013: } catch (InterruptedIOException iioex) {
1014: /*
1015: * As above in simpleCommand, close the socket to recover.
1016: */
1017: try {
1018: socket.close();
1019: } catch (IOException cex) {
1020: }
1021: throw iioex;
1022: }
1023: if (b < 0)
1024: throw new EOFException("EOF on socket");
1025:
1026: // was there a deferred error?
1027: if (terr != null) {
1028: if (terr instanceof IOException)
1029: throw (IOException) terr;
1030: if (terr instanceof RuntimeException)
1031: throw (RuntimeException) terr;
1032: assert false; // can't get here
1033: }
1034: multilineCommandEnd();
1035: return true;
1036: }
1037:
1038: /**
1039: * Return the message header and the first n lines of the message.
1040: */
1041: synchronized InputStream top(int msg, int n) throws IOException {
1042: Response r = multilineCommand("TOP " + msg + " " + n, 0);
1043: return r.bytes;
1044: }
1045:
1046: /**
1047: * Delete (permanently) the specified message.
1048: */
1049: synchronized boolean dele(int msg) throws IOException {
1050: Response r = simpleCommand("DELE " + msg);
1051: return r.ok;
1052: }
1053:
1054: /**
1055: * Return the UIDL string for the message.
1056: */
1057: synchronized String uidl(int msg) throws IOException {
1058: Response r = simpleCommand("UIDL " + msg);
1059: if (!r.ok)
1060: return null;
1061: int i = r.data.indexOf(' ');
1062: if (i > 0)
1063: return r.data.substring(i + 1);
1064: else
1065: return null;
1066: }
1067:
1068: /**
1069: * Return the UIDL strings for all messages.
1070: * The UID for msg #N is returned in uids[N-1].
1071: */
1072: synchronized boolean uidl(String[] uids) throws IOException {
1073: Response r = multilineCommand("UIDL", 15 * uids.length);
1074: if (!r.ok)
1075: return false;
1076: LineInputStream lis = new LineInputStream(r.bytes);
1077: String line = null;
1078: while ((line = lis.readLine()) != null) {
1079: int i = line.indexOf(' ');
1080: if (i < 1 || i >= line.length())
1081: continue;
1082: int n = Integer.parseInt(line.substring(0, i));
1083: if (n > 0 && n <= uids.length)
1084: uids[n - 1] = line.substring(i + 1);
1085: }
1086: try {
1087: r.bytes.close();
1088: } catch (IOException ex) {
1089: // ignore it
1090: }
1091: return true;
1092: }
1093:
1094: /**
1095: * Do a NOOP.
1096: */
1097: synchronized boolean noop() throws IOException {
1098: Response r = simpleCommand("NOOP");
1099: return r.ok;
1100: }
1101:
1102: /**
1103: * Do an RSET.
1104: */
1105: synchronized boolean rset() throws IOException {
1106: Response r = simpleCommand("RSET");
1107: return r.ok;
1108: }
1109:
1110: /**
1111: * Start TLS using STLS command specified by RFC 2595.
1112: * If already using SSL, this is a nop and the STLS command is not issued.
1113: */
1114: synchronized boolean stls() throws IOException {
1115: if (socket instanceof SSLSocket)
1116: return true; // nothing to do
1117: Response r = simpleCommand("STLS");
1118: if (r.ok) {
1119: // it worked, now switch the socket into TLS mode
1120: try {
1121: socket = SocketFetcher.startTLS(socket, host, props, prefix);
1122: initStreams();
1123: } catch (IOException ioex) {
1124: try {
1125: socket.close();
1126: } finally {
1127: socket = null;
1128: input = null;
1129: output = null;
1130: }
1131: IOException sioex =
1132: new IOException("Could not convert socket to TLS");
1133: sioex.initCause(ioex);
1134: throw sioex;
1135: }
1136: }
1137: return r.ok;
1138: }
1139:
1140: /**
1141: * Is this connection using SSL?
1142: */
1143: synchronized boolean isSSL() {
1144: return socket instanceof SSLSocket;
1145: }
1146:
1147: /**
1148: * Get server capabilities using CAPA command specified by RFC 2449.
1149: * Returns null if not supported.
1150: */
1151: synchronized InputStream capa() throws IOException {
1152: Response r = multilineCommand("CAPA", 128); // 128 == output size est
1153: if (!r.ok)
1154: return null;
1155: return r.bytes;
1156: }
1157:
1158: /**
1159: * Issue a simple POP3 command and return the response.
1160: */
1161: private Response simpleCommand(String cmd) throws IOException {
1162: simpleCommandStart(cmd);
1163: issueCommand(cmd);
1164: Response r = readResponse();
1165: simpleCommandEnd();
1166: return r;
1167: }
1168:
1169: /**
1170: * Issue a two line POP3 command and return the response
1171: * Refer to {@link #simpleCommand(String)} for a single line command
1172: *
1173: * @param firstCommand first command we want to pass to server e.g AUTH XOAUTH2
1174: * @param secondCommand second command e.g Base64 encoded authorization string
1175: * @return Response
1176: */
1177: private Response twoLinesCommand(String firstCommand, String secondCommand) throws IOException {
1178: String cmd = firstCommand + " " + secondCommand;
1179:
1180: batchCommandStart(cmd);
1181: simpleCommand(firstCommand);
1182: batchCommandContinue(cmd);
1183:
1184: Response r = simpleCommand(secondCommand);
1185:
1186: batchCommandEnd();
1187:
1188: return r;
1189: }
1190:
1191: /**
1192: * Send the specified command.
1193: */
1194: private void issueCommand(String cmd) throws IOException {
1195: if (socket == null)
1196: throw new IOException("Folder is closed"); // XXX
1197:
1198: if (cmd != null) {
1199: cmd += CRLF;
1200: output.print(cmd); // do it in one write
1201: output.flush();
1202: }
1203: }
1204:
1205: /**
1206: * Read the response to a command.
1207: */
1208: private Response readResponse() throws IOException {
1209: String line = null;
1210: try {
1211: line = input.readLine();
1212: } catch (InterruptedIOException iioex) {
1213: /*
1214: * If we get a timeout while using the socket, we have no idea
1215: * what state the connection is in. The server could still be
1216: * alive, but slow, and could still be sending data. The only
1217: * safe way to recover is to drop the connection.
1218: */
1219: try {
1220: socket.close();
1221: } catch (IOException cex) {
1222: }
1223: throw new EOFException(iioex.getMessage());
1224: } catch (SocketException ex) {
1225: /*
1226: * If we get an error while using the socket, we have no idea
1227: * what state the connection is in. The server could still be
1228: * alive, but slow, and could still be sending data. The only
1229: * safe way to recover is to drop the connection.
1230: */
1231: try {
1232: socket.close();
1233: } catch (IOException cex) {
1234: }
1235: throw new EOFException(ex.getMessage());
1236: }
1237:
1238: if (line == null) {
1239: traceLogger.finest("<EOF>");
1240: throw new EOFException("EOF on socket");
1241: }
1242: Response r = new Response();
1243: if (line.startsWith("+OK"))
1244: r.ok = true;
1245: else if (line.startsWith("+ ")) {
1246: r.ok = true;
1247: r.cont = true;
1248: } else if (line.startsWith("-ERR"))
1249: r.ok = false;
1250: else
1251: throw new IOException("Unexpected response: " + line);
1252: int i;
1253: if ((i = line.indexOf(' ')) >= 0)
1254: r.data = line.substring(i + 1);
1255: return r;
1256: }
1257:
1258: /**
1259: * Issue a POP3 command that expects a multi-line response.
1260: * <code>size</code> is an estimate of the response size.
1261: */
1262: private Response multilineCommand(String cmd, int size) throws IOException {
1263: multilineCommandStart(cmd);
1264: issueCommand(cmd);
1265: Response r = readResponse();
1266: if (!r.ok) {
1267: multilineCommandEnd();
1268: return r;
1269: }
1270: r.bytes = readMultilineResponse(size);
1271: multilineCommandEnd();
1272: return r;
1273: }
1274:
1275: /**
1276: * Read the response to a multiline command after the command response.
1277: * The size parameter indicates the expected size of the response;
1278: * the actual size can be different. Returns an InputStream to the
1279: * response bytes.
1280: */
1281: private InputStream readMultilineResponse(int size) throws IOException {
1282: SharedByteArrayOutputStream buf = new SharedByteArrayOutputStream(size);
1283: int b, lastb = '\n';
1284: try {
1285: while ((b = input.read()) >= 0) {
1286: if (lastb == '\n' && b == '.') {
1287: b = input.read();
1288: if (b == '\r') {
1289: // end of response, consume LF as well
1290: b = input.read();
1291: break;
1292: }
1293: }
1294: buf.write(b);
1295: lastb = b;
1296: }
1297: } catch (InterruptedIOException iioex) {
1298: /*
1299: * As above in readResponse, close the socket to recover.
1300: */
1301: try {
1302: socket.close();
1303: } catch (IOException cex) {
1304: }
1305: throw iioex;
1306: }
1307: if (b < 0)
1308: throw new EOFException("EOF on socket");
1309: return buf.toStream();
1310: }
1311:
1312: /**
1313: * Is protocol tracing enabled?
1314: */
1315: protected boolean isTracing() {
1316: return traceLogger.isLoggable(Level.FINEST);
1317: }
1318:
1319: /**
1320: * Temporarily turn off protocol tracing, e.g., to prevent
1321: * tracing the authentication sequence, including the password.
1322: */
1323: private void suspendTracing() {
1324: if (traceLogger.isLoggable(Level.FINEST)) {
1325: traceInput.setTrace(false);
1326: traceOutput.setTrace(false);
1327: }
1328: }
1329:
1330: /**
1331: * Resume protocol tracing, if it was enabled to begin with.
1332: */
1333: private void resumeTracing() {
1334: if (traceLogger.isLoggable(Level.FINEST)) {
1335: traceInput.setTrace(true);
1336: traceOutput.setTrace(true);
1337: }
1338: }
1339:
1340: /*
1341: * Probe points for GlassFish monitoring.
1342: */
1343: private void simpleCommandStart(String command) {
1344: }
1345:
1346: private void simpleCommandEnd() {
1347: }
1348:
1349: private void multilineCommandStart(String command) {
1350: }
1351:
1352: private void multilineCommandEnd() {
1353: }
1354:
1355: private void batchCommandStart(String command) {
1356: }
1357:
1358: private void batchCommandContinue(String command) {
1359: }
1360:
1361: private void batchCommandEnd() {
1362: }
1363: }