private List<String> executeSimpleCommand(String command, boolean sensitive) throws IOException, MessagingException { List<String> results = new ArrayList<String>(); if (command != null) { writeLine(command, sensitive); } /* * Read lines as long as the length is 4 or larger, e.g. "220-banner text here". * Shorter lines are either errors of contain only a reply code. Those cases will * be handled by checkLine() below. */ String line = readLine(); while (line.length() >= 4) { if (line.length() > 4) { // Everything after the first four characters goes into the results array. results.add(line.substring(4)); } if (line.charAt(3) != '-') { // If the fourth character isn't "-" this is the last line of the response. break; } line = readLine(); } // Check if the reply code indicates an error. checkLine(line); return results; }
private void saslAuthCramMD5(String username, String password) throws MessagingException, AuthenticationFailedException, IOException { List<String> respList = executeSimpleCommand("AUTH CRAM-MD5"); if (respList.size() != 1) { throw new AuthenticationFailedException("Unable to negotiate CRAM-MD5"); } String b64Nonce = respList.get(0); String b64CRAMString = Authentication.computeCramMd5(mUsername, mPassword, b64Nonce); try { executeSimpleCommand(b64CRAMString, true); } catch (MessagingException me) { throw new AuthenticationFailedException("Unable to negotiate MD5 CRAM"); } }
@Override public void open() throws MessagingException { try { InetAddress[] addresses = InetAddress.getAllByName(mHost); for (int i = 0; i < addresses.length; i++) { try { SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort); if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED || mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { SSLContext sslContext = SSLContext.getInstance("TLS"); boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; sslContext.init( null, new TrustManager[] {TrustManagerFactory.get(mHost, secure)}, new SecureRandom()); mSocket = sslContext.getSocketFactory().createSocket(); mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); mSecure = true; } else { mSocket = new Socket(); mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); } } catch (ConnectException e) { if (i < (addresses.length - 1)) { // there are still other addresses for that host to try continue; } throw new MessagingException("Cannot connect to host", e); } break; // connection success } // RFC 1047 mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024)); mOut = mSocket.getOutputStream(); // Eat the banner executeSimpleCommand(null); InetAddress localAddress = mSocket.getLocalAddress(); String localHost = localAddress.getCanonicalHostName(); String ipAddr = localAddress.getHostAddress(); if (localHost.equals("") || localHost.equals(ipAddr) || localHost.contains("_")) { // We don't have a FQDN or the hostname contains invalid // characters (see issue 2143), so use IP address. if (!ipAddr.equals("")) { if (localAddress instanceof Inet6Address) { localHost = "[IPV6:" + ipAddr + "]"; } else { localHost = "[" + ipAddr + "]"; } } else { // If the IP address is no good, set a sane default (see issue 2750). localHost = "android"; } } List<String> results = executeSimpleCommand("EHLO " + localHost); m8bitEncodingAllowed = results.contains("8BITMIME"); /* * TODO may need to add code to fall back to HELO I switched it from * using HELO on non STARTTLS connections because of AOL's mail * server. It won't let you use AUTH without EHLO. * We should really be paying more attention to the capabilities * and only attempting auth if it's available, and warning the user * if not. */ if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { if (results.contains("STARTTLS")) { executeSimpleCommand("STARTTLS"); SSLContext sslContext = SSLContext.getInstance("TLS"); boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; sslContext.init( null, new TrustManager[] {TrustManagerFactory.get(mHost, secure)}, new SecureRandom()); mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort, true); mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024)); mOut = mSocket.getOutputStream(); mSecure = true; /* * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, * Exim. */ results = executeSimpleCommand("EHLO " + localHost); } else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { throw new MessagingException("TLS not supported but required"); } } boolean useAuthLogin = AUTH_LOGIN.equals(mAuthType); boolean useAuthPlain = AUTH_PLAIN.equals(mAuthType); boolean useAuthCramMD5 = AUTH_CRAM_MD5.equals(mAuthType); // Automatically choose best authentication method if none was explicitly selected boolean useAutomaticAuth = !(useAuthLogin || useAuthPlain || useAuthCramMD5); boolean authLoginSupported = false; boolean authPlainSupported = false; boolean authCramMD5Supported = false; for (String result : results) { if (result.matches(".*AUTH.*LOGIN.*$")) { authLoginSupported = true; } if (result.matches(".*AUTH.*PLAIN.*$")) { authPlainSupported = true; } if (result.matches(".*AUTH.*CRAM-MD5.*$")) { authCramMD5Supported = true; } if (result.matches(".*SIZE \\d*$")) { try { mLargestAcceptableMessage = Integer.parseInt(result.substring(result.lastIndexOf(' ') + 1)); } catch (Exception e) { if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) { Log.d( K9.LOG_TAG, "Tried to parse " + result + " and get an int out of the last word", e); } } } } if (mUsername != null && mUsername.length() > 0 && mPassword != null && mPassword.length() > 0) { if (useAuthCramMD5 || (useAutomaticAuth && authCramMD5Supported)) { if (!authCramMD5Supported && K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) { Log.d( K9.LOG_TAG, "Using CRAM_MD5 as authentication method although the " + "server didn't advertise support for it in EHLO response."); } saslAuthCramMD5(mUsername, mPassword); } else if (useAuthPlain || (useAutomaticAuth && authPlainSupported)) { if (!authPlainSupported && K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) { Log.d( K9.LOG_TAG, "Using PLAIN as authentication method although the " + "server didn't advertise support for it in EHLO response."); } try { saslAuthPlain(mUsername, mPassword); } catch (MessagingException ex) { // PLAIN is a special case. Historically, PLAIN has represented both PLAIN and LOGIN; // only the // protocol being advertised by the server would be used, with PLAIN taking precedence. // Instead // of using only the requested protocol, we'll try PLAIN and then try LOGIN. if (useAuthPlain && authLoginSupported) { if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) { Log.d(K9.LOG_TAG, "Using legacy PLAIN authentication behavior and trying LOGIN."); } saslAuthLogin(mUsername, mPassword); } else { // If it was auto detected and failed, continue throwing the exception back up. throw ex; } } } else if (useAuthLogin || (useAutomaticAuth && authLoginSupported)) { if (!authPlainSupported && K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) { Log.d( K9.LOG_TAG, "Using LOGIN as authentication method although the " + "server didn't advertise support for it in EHLO response."); } saslAuthLogin(mUsername, mPassword); } else { throw new MessagingException("No valid authentication mechanism found."); } } } catch (SSLException e) { throw new CertificateValidationException(e.getMessage(), e); } catch (GeneralSecurityException gse) { throw new MessagingException( "Unable to open connection to SMTP server due to security error.", gse); } catch (IOException ioe) { throw new MessagingException("Unable to open connection to SMTP server.", ioe); } }