From 786fde2ca40bc699b8f32f02fbfdef3c7ae1bb1e Mon Sep 17 00:00:00 2001 From: M66B Date: Tue, 17 Mar 2020 12:01:34 +0100 Subject: [PATCH] Updated Javamail to version 1.6.5 --- app/build.gradle | 2 +- .../java/com/sun/mail/pop3/POP3Store.java | 126 +++++- .../main/java/com/sun/mail/pop3/Protocol.java | 419 +++++++++++++++++- .../main/java/com/sun/mail/pop3/package.html | 95 +++- app/src/main/java/javax/mail/Version.java | 2 +- 5 files changed, 636 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 322c93b07f..17ed7cfa6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { def exif_version = "1.3.0-alpha01" def biometric_version = "1.0.1" def billingclient_version = "2.1.0" - def javamail_version = "1.6.5-SNAPSHOT" + def javamail_version = "1.6.5" def jsoup_version = "1.12.1" def dnsjava_version = "2.1.9" def openpgp_version = "12.0" diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Store.java b/app/src/main/java/com/sun/mail/pop3/POP3Store.java index c9818a1de7..2c8330af9b 100644 --- a/app/src/main/java/com/sun/mail/pop3/POP3Store.java +++ b/app/src/main/java/com/sun/mail/pop3/POP3Store.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -16,7 +16,9 @@ package com.sun.mail.pop3; +import java.util.Locale; import java.util.Properties; +import java.util.StringTokenizer; import java.util.logging.Level; import java.lang.reflect.*; @@ -280,11 +282,16 @@ public class POP3Store extends Store { supportsUidl = capabilities == null || capabilities.containsKey("UIDL"); - String msg = null; - if ((msg = p.login(user, passwd)) != null) { - throw cleanupAndThrow(p, new EOFException(msg)); + try { + if (!authenticate(p, user, passwd)) + throw cleanupAndThrow(p, new EOFException("login failed")); + } catch (EOFException ex) { + throw cleanupAndThrow(p, ex); + } catch (Exception ex) { + throw cleanupAndThrow(p, new EOFException(ex.getMessage())); } + /* * If a Folder closes the port, and then a Folder * is opened, the Store won't have a port. In that @@ -322,6 +329,117 @@ public class POP3Store extends Store { return ife; } + /** + * Authenticate to the server. + * + * XXX - This extensible authentication mechanism scheme was adapted + * from the SMTPTransport class. The work was done at the last + * minute for the 1.6.5 release and so is not as clean as it + * could be. There's great confusion over boolean success/failure + * return codes vs exceptions. This should all be cleaned up at + * some point, and more testing should be done, but I'm leaving + * it in this "I believe it works" state for now. I've tested + * it with LOGIN, PLAIN, and XOAUTH2 mechanisms, the latter being + * the primary motivation for the work right now. + * + * @param p the Protocol object to use + * @param user the user to authenticate as + * @param passwd the password for the user + * @return true if authentication succeeds + * @exception MessagingException if authentication fails + * @since Jakarta Mail 1.6.5 + */ + private boolean authenticate(Protocol p, String user, String passwd) + throws MessagingException { + // setting mail.pop3.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + boolean usingDefaultMechs = false; + if (mechs == null) { + mechs = p.getDefaultMechanisms(); + usingDefaultMechs = true; + } + + String authzid = + session.getProperty("mail." + name + ".sasl.authorizationid"); + if (authzid == null) + authzid = user; + /* + * XXX - maybe someday + * + if (enableSASL) { + logger.fine("Authenticate with SASL"); + try { + if (sasllogin(getSASLMechanisms(), getSASLRealm(), authzid, + user, passwd)) { + return true; // success + } else { + logger.fine("SASL authentication failed"); + return false; + } + } catch (UnsupportedOperationException ex) { + logger.log(Level.FINE, "SASL support failed", ex); + // if the SASL support fails, fall back to non-SASL + } + } + */ + + if (logger.isLoggable(Level.FINE)) + logger.fine("Attempt to authenticate using mechanisms: " + mechs); + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + if (!p.supportsMechanism(m)) { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + + if (!p.supportsAuthentication(m)) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (usingDefaultMechs) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, !p.isMechanismEnabled(m)); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + // only the first supported and enabled mechanism is used + logger.log(Level.FINE, "Using mechanism {0}", m); + String msg = + p.authenticate(m, host, authzid, user, passwd); + if (msg != null) + throw new AuthenticationFailedException(msg); + return true; + } + + // if no authentication mechanism found, fail + throw new AuthenticationFailedException( + "No authentication mechanisms supported by both server and client"); + } + private static boolean isRecoverable(Throwable t) { return (t instanceof Exception) || (t instanceof LinkageError); } diff --git a/app/src/main/java/com/sun/mail/pop3/Protocol.java b/app/src/main/java/com/sun/mail/pop3/Protocol.java index 85c9a0f953..fb0a05aaa5 100644 --- a/app/src/main/java/com/sun/mail/pop3/Protocol.java +++ b/app/src/main/java/com/sun/mail/pop3/Protocol.java @@ -21,8 +21,13 @@ import java.net.*; import java.io.*; import java.security.*; import java.util.logging.Level; +import java.nio.charset.StandardCharsets; import javax.net.ssl.SSLSocket; +import com.sun.mail.auth.Ntlm; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.BASE64EncoderStream; import com.sun.mail.util.PropUtil; import com.sun.mail.util.MailLogger; import com.sun.mail.util.SocketFetcher; @@ -33,6 +38,7 @@ import com.sun.mail.util.SharedByteArrayOutputStream; class Response { boolean ok = false; // true if "+OK" + boolean cont = false; // true if "+ " continuation line String data = null; // rest of line after "+OK" or "-ERR" InputStream bytes = null; // all the bytes from a multi-line response } @@ -61,6 +67,9 @@ class Protocol { private boolean pipelining; private boolean noauthdebug = true; // hide auth info in debug output private boolean traceSuspended; // temporarily suspend tracing + private Map authenticators = new HashMap<>(); + private String defaultAuthenticationMechanisms; // set in constructor + private String localHostName; private static final int POP3_PORT = 110; // standard POP3 port private static final String CRLF = "\r\n"; @@ -117,6 +126,21 @@ class Protocol { PropUtil.getBooleanProperty(props, prefix + ".pipelining", false); if (pipelining) logger.config("PIPELINING enabled"); + + // created here, because they're inner classes that reference "this" + Authenticator[] a = new Authenticator[] { + new LoginAuthenticator(), + new PlainAuthenticator(), + //new DigestMD5Authenticator(), + new NtlmAuthenticator(), + new OAuth2Authenticator() + }; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < a.length; i++) { + authenticators.put(a[i].getMechanism(), a[i]); + sb.append(a[i].getMechanism()).append(' '); + } + defaultAuthenticationMechanisms = sb.toString(); } private static IOException cleanupAndThrow(Socket socket, IOException ife) { @@ -236,6 +260,84 @@ class Protocol { return capabilities; } + /** + * Does this Protocol object support the named authentication mechanism? + * + * @since Jakarta Mail 1.6.5 + */ + boolean supportsMechanism(String mech) { + return authenticators.containsKey(mech.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the whitespace separated string list of default authentication + * mechanisms. + * + * @since Jakarta Mail 1.6.5 + */ + String getDefaultMechanisms() { + return defaultAuthenticationMechanisms; + } + + /** + * Is the named authentication mechanism enabled? + * + * @since Jakarta Mail 1.6.5 + */ + boolean isMechanismEnabled(String mech) { + Authenticator a = authenticators.get(mech.toUpperCase(Locale.ENGLISH)); + return a != null && a.enabled(); + } + + /** + * Authenticate to the server using the named authentication mechanism + * and the supplied credentials. + * + * @since Jakarta Mail 1.6.5 + */ + synchronized String authenticate(String mech, + String host, String authzid, + String user, String passwd) { + Authenticator a = authenticators.get(mech.toUpperCase(Locale.ENGLISH)); + if (a == null) + return "No such authentication mechanism: " + mech; + try { + if (!a.authenticate(host, authzid, user, passwd)) + return "login failed"; + return null; + } catch (IOException ex) { + return ex.getMessage(); + } + } + + /** + * Does the server we're connected to support the specified + * authentication mechanism? Uses the information + * returned by the server from the CAPA command. + * + * @param auth the authentication mechanism + * @return true if the authentication mechanism is supported + * + * @since Jakarta Mail 1.6.5 + */ + synchronized boolean supportsAuthentication(String auth) { + assert Thread.holdsLock(this); + if (auth.equals("LOGIN")) + return true; + if (capabilities == null) + return false; + String a = capabilities.get("SASL"); + if (a == null) + return false; + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equalsIgnoreCase(auth)) + return true; + } + return false; + } + /** * Login to the server, using the USER and PASS commands. */ @@ -317,6 +419,318 @@ class Protocol { return toHex(digest); } + /** + * Abstract base class for POP3 authentication mechanism implementations. + * + * @since Jakarta Mail 1.6.5 + */ + private abstract class Authenticator { + protected Response resp; // the response, used by subclasses + private final String mech; // the mechanism name, set in the constructor + private final boolean enabled; // is this mechanism enabled by default? + + Authenticator(String mech) { + this(mech, true); + } + + Authenticator(String mech, boolean enabled) { + this.mech = mech.toUpperCase(Locale.ENGLISH); + this.enabled = enabled; + } + + String getMechanism() { + return mech; + } + + boolean enabled() { + return enabled; + } + + /** + * Start the authentication handshake by issuing the AUTH command. + * Delegate to the doAuth method to do the mechanism-specific + * part of the handshake. + */ + boolean authenticate(String host, String authzid, + String user, String passwd) throws IOException { + Throwable thrown = null; + try { + // use "initial response" capability, if supported + String ir = getInitialResponse(host, authzid, user, passwd); + if (noauthdebug && isTracing()) { + logger.fine("AUTH " + mech + " command trace suppressed"); + suspendTracing(); + } + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + + (ir.length() == 0 ? "=" : ir)); + else + resp = simpleCommand("AUTH " + mech); + + if (resp.cont) + doAuth(host, authzid, user, passwd); + } catch (IOException ex) { // should never happen, ignore + logger.log(Level.FINE, "AUTH " + mech + " failed", ex); + } catch (Throwable t) { // crypto can't be initialized? + logger.log(Level.FINE, "AUTH " + mech + " failed", t); + thrown = t; + } finally { + if (noauthdebug && isTracing()) + logger.fine("AUTH " + mech + " " + + (resp.ok ? "succeeded" : "failed")); + resumeTracing(); + if (!resp.ok) { + close(); + if (thrown != null) { + if (thrown instanceof Error) + throw (Error)thrown; + if (thrown instanceof Exception) { + EOFException ex = new EOFException( + resp.data != null ? + resp.data : "authentication failed"); + ex.initCause(thrown); + throw ex; + } + assert false : "unknown Throwable"; // can't happen + } + throw new EOFException(resp.data != null ? + resp.data : "authentication failed"); + } + } + return true; + } + + /** + * Provide the initial response to use in the AUTH command, + * or null if not supported. Subclasses that support the + * initial response capability will override this method. + */ + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + return null; + } + + abstract void doAuth(String host, String authzid, String user, + String passwd) throws IOException; + } + + /** + * Perform the authentication handshake for LOGIN authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class LoginAuthenticator extends Authenticator { + LoginAuthenticator() { + super("LOGIN"); + } + + @Override + boolean authenticate(String host, String authzid, + String user, String passwd) throws IOException { + String msg = null; + if ((msg = login(user, passwd)) != null) { + throw new EOFException(msg); + } + return true; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + // should never get here + throw new EOFException("LOGIN asked for more"); + } + } + + /** + * Perform the authentication handshake for PLAIN authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class PlainAuthenticator extends Authenticator { + PlainAuthenticator() { + super("PLAIN"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + // return "authziduserpasswd" + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = + new BASE64EncoderStream(bos, Integer.MAX_VALUE); + if (authzid != null) + b64os.write(authzid.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(user.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(passwd.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + return ASCIIUtility.toString(bos.toByteArray()); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + // should never get here + throw new EOFException("PLAIN asked for more"); + } + } + + /** + * Perform the authentication handshake for DIGEST-MD5 authentication. + * + * @since Jakarta Mail 1.6.5 + */ + /* + * XXX - Need to move DigestMD5 class to com.sun.mail.auth + * + private class DigestMD5Authenticator extends Authenticator { + private DigestMD5 md5support; // only create if needed + + DigestMD5Authenticator() { + super("DIGEST-MD5"); + } + + private synchronized DigestMD5 getMD5() { + if (md5support == null) + md5support = new DigestMD5(logger); + return md5support; + } + + @Override + void doAuth(Protocol p, String host, String authzid, + String user, String passwd) + throws IOException { + DigestMD5 md5 = getMD5(); + assert md5 != null; + + byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), + resp.data); + resp = p.simpleCommand(b); + if (resp.cont) { // client authenticated by server + if (!md5.authServer(resp.data)) { + // server NOT authenticated by client !!! + resp.ok = false; + } else { + // send null response + resp = simpleCommand(new byte[0]); + } + } + } + } + */ + + /** + * Perform the authentication handshake for NTLM authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class NtlmAuthenticator extends Authenticator { + private Ntlm ntlm; + + NtlmAuthenticator() { + super("NTLM"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + ntlm = new Ntlm(props.getProperty(prefix + ".auth.ntlm.domain"), + getLocalHost(), user, passwd, logger); + + int flags = PropUtil.getIntProperty( + props, prefix + ".auth.ntlm.flags", 0); + boolean v2 = PropUtil.getBooleanProperty( + props, prefix + ".auth.ntlm.v2", true); + + String type1 = ntlm.generateType1Msg(flags, v2); + return type1; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + assert ntlm != null; + String type3 = ntlm.generateType3Msg( + resp.data.substring(4).trim()); + + resp = simpleCommand(type3); + } + } + + /** + * Perform the authentication handshake for XOAUTH2 authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class OAuth2Authenticator extends Authenticator { + + OAuth2Authenticator() { + super("XOAUTH2", false); // disabled by default + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + String resp = "user=" + user + "\001auth=Bearer " + + passwd + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + return ASCIIUtility.toString(b); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + // OAuth2 failure returns a JSON error code, + // which looks like a "please continue" to the authenticate() + // code, so we turn that into a clean failure here. + String err = ""; + if (resp.data != null) { + byte[] b = resp.data.getBytes(StandardCharsets.UTF_8); + b = BASE64DecoderStream.decode(b); + err = new String(b, StandardCharsets.UTF_8); + } + throw new EOFException("OAUTH2 authentication failed: " + err); + } + } + + /** + * Get the name of the local host. + * + * @return the local host name + * @since Jakarta Mail 1.6.5 + */ + private synchronized String getLocalHost() { + // get our hostname and cache it for future use + try { + if (localHostName == null || localHostName.length() == 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (socket != null && socket.isBound()) { + InetAddress localHost = socket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + private static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' @@ -758,7 +1172,10 @@ class Protocol { Response r = new Response(); if (line.startsWith("+OK")) r.ok = true; - else if (line.startsWith("-ERR")) + else if (line.startsWith("+ ")) { + r.ok = true; + r.cont = true; + } else if (line.startsWith("-ERR")) r.ok = false; else throw new IOException("Unexpected response: " + line); diff --git a/app/src/main/java/com/sun/mail/pop3/package.html b/app/src/main/java/com/sun/mail/pop3/package.html index a3ea13b8eb..0a01a78be0 100644 --- a/app/src/main/java/com/sun/mail/pop3/package.html +++ b/app/src/main/java/com/sun/mail/pop3/package.html @@ -4,7 +4,7 @@ + + +mail.pop3.auth.xoauth2.disable +boolean +If true, prevents use of the AUTHENTICATE XOAUTH2 command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.pop3.auth.mechanisms" +property to "XOAUTH2". + + mail.pop3.socketFactory SocketFactory diff --git a/app/src/main/java/javax/mail/Version.java b/app/src/main/java/javax/mail/Version.java index 9f1793abca..17baeae281 100644 --- a/app/src/main/java/javax/mail/Version.java +++ b/app/src/main/java/javax/mail/Version.java @@ -22,5 +22,5 @@ package javax.mail; * at build time. */ class Version { - public static final String version = "1.6.5-SNAPSHOT"; + public static final String version = "1.6.5"; }