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