package com.fsck.k9.mail.store;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.UrlEncodingHelper;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.internet.MimeMessage;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import javax.net.ssl.SSLException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.GZIPInputStream;
/**
*
* Uses WebDAV formatted HTTP calls to an MS Exchange server to fetch email
* and email information.
*
*/
public class WebDavStore extends RemoteStore {
public static final String STORE_TYPE = "WebDAV";
// Authentication types
private static final short AUTH_TYPE_NONE = 0;
private static final short AUTH_TYPE_BASIC = 1;
private static final short AUTH_TYPE_FORM_BASED = 2;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
// These are the ids used from Exchange server to identify the special folders
// http://social.technet.microsoft.com/Forums/en/exchangesvrdevelopment/thread/1cd2e98c-8a12-44bd-a3e3-9c5ee9e4e14d
private static final String DAV_MAIL_INBOX_FOLDER = "inbox";
private static final String DAV_MAIL_DRAFTS_FOLDER = "drafts";
private static final String DAV_MAIL_SPAM_FOLDER = "junkemail";
private static final String DAV_MAIL_SEND_FOLDER = "##DavMailSubmissionURI##";
private static final String DAV_MAIL_TRASH_FOLDER = "deleteditems";
private static final String DAV_MAIL_OUTBOX_FOLDER = "outbox";
private static final String DAV_MAIL_SENT_FOLDER = "sentitems";
/**
* Decodes a WebDavStore URI.
*
* Possible forms:
*
* webdav://user:password@server:port ConnectionSecurity.NONE
* webdav+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
*
*/
public static WebDavStoreSettings decodeUri(String uri) {
String host;
int port;
ConnectionSecurity connectionSecurity;
String username = null;
String password = null;
String alias = null;
String path = null;
String authPath = null;
String mailboxPath = null;
URI webDavUri;
try {
webDavUri = new URI(uri);
} catch (URISyntaxException use) {
throw new IllegalArgumentException("Invalid WebDavStore URI", use);
}
String scheme = webDavUri.getScheme();
/*
* Currently available schemes are:
* webdav
* webdav+ssl+
*
* The following are obsolete schemes that may be found in pre-existing
* settings from earlier versions or that may be found when imported. We
* continue to recognize them and re-map them appropriately:
* webdav+tls
* webdav+tls+
* webdav+ssl
*/
if (scheme.equals("webdav")) {
connectionSecurity = ConnectionSecurity.NONE;
} else if (scheme.startsWith("webdav+")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
} else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
host = webDavUri.getHost();
if (host.startsWith("http")) {
String[] hostParts = host.split("://", 2);
if (hostParts.length > 1) {
host = hostParts[1];
}
}
port = webDavUri.getPort();
String userInfo = webDavUri.getUserInfo();
if (userInfo != null) {
String[] userInfoParts = userInfo.split(":");
username = UrlEncodingHelper.decodeUtf8(userInfoParts[0]);
String userParts[] = username.split("\\\\", 2);
if (userParts.length > 1) {
alias = userParts[1];
} else {
alias = username;
}
if (userInfoParts.length > 1) {
password = UrlEncodingHelper.decodeUtf8(userInfoParts[1]);
}
}
String[] pathParts = webDavUri.getPath().split("\\|");
for (int i = 0, count = pathParts.length; i < count; i++) {
if (i == 0) {
if (pathParts[0] != null &&
pathParts[0].length() > 1) {
path = pathParts[0];
}
} else if (i == 1) {
if (pathParts[1] != null &&
pathParts[1].length() > 1) {
authPath = pathParts[1];
}
} else if (i == 2) {
if (pathParts[2] != null &&
pathParts[2].length() > 1) {
mailboxPath = pathParts[2];
}
}
}
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
null, alias, path, authPath, mailboxPath);
}
/**
* Creates a WebDavStore URI with the supplied settings.
*
* @param server
* The {@link ServerSettings} object that holds the server settings.
*
* @return A WebDavStore URI that holds the same information as the {@code server} parameter.
*
* @see Account#getStoreUri()
* @see WebDavStore#decodeUri(String)
*/
public static String createUri(ServerSettings server) {
String userEnc = UrlEncodingHelper.encodeUtf8(server.username);
String passwordEnc = (server.password != null) ?
UrlEncodingHelper.encodeUtf8(server.password) : "";
String scheme;
switch (server.connectionSecurity) {
case SSL_TLS_REQUIRED:
scheme = "webdav+ssl+";
break;
default:
case NONE:
scheme = "webdav";
break;
}
String userInfo = userEnc + ":" + passwordEnc;
String uriPath;
Map extra = server.getExtra();
if (extra != null) {
String path = extra.get(WebDavStoreSettings.PATH_KEY);
path = (path != null) ? path : "";
String authPath = extra.get(WebDavStoreSettings.AUTH_PATH_KEY);
authPath = (authPath != null) ? authPath : "";
String mailboxPath = extra.get(WebDavStoreSettings.MAILBOX_PATH_KEY);
mailboxPath = (mailboxPath != null) ? mailboxPath : "";
uriPath = "/" + path + "|" + authPath + "|" + mailboxPath;
} else {
uriPath = "/||";
}
try {
return new URI(scheme, userInfo, server.host, server.port, uriPath,
null, null).toString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Can't create WebDavStore URI", e);
}
}
/**
* This class is used to store the decoded contents of an WebDavStore URI.
*
* @see WebDavStore#decodeUri(String)
*/
public static class WebDavStoreSettings extends ServerSettings {
public static final String ALIAS_KEY = "alias";
public static final String PATH_KEY = "path";
public static final String AUTH_PATH_KEY = "authPath";
public static final String MAILBOX_PATH_KEY = "mailboxPath";
public final String alias;
public final String path;
public final String authPath;
public final String mailboxPath;
protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
AuthType authenticationType, String username, String password, String clientCertificateAlias, String alias,
String path, String authPath, String mailboxPath) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
password, clientCertificateAlias);
this.alias = alias;
this.path = path;
this.authPath = authPath;
this.mailboxPath = mailboxPath;
}
@Override
public Map getExtra() {
Map extra = new HashMap();
putIfNotNull(extra, ALIAS_KEY, alias);
putIfNotNull(extra, PATH_KEY, path);
putIfNotNull(extra, AUTH_PATH_KEY, authPath);
putIfNotNull(extra, MAILBOX_PATH_KEY, mailboxPath);
return extra;
}
@Override
public ServerSettings newPassword(String newPassword) {
return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType,
username, newPassword, clientCertificateAlias, alias, path, authPath, mailboxPath);
}
}
private ConnectionSecurity mConnectionSecurity;
private String mUsername; /* Stores the username for authentications */
private String mAlias; /* Stores the alias for the user's mailbox */
private String mPassword; /* Stores the password for authentications */
private String mUrl; /* Stores the base URL for the server */
private String mHost; /* Stores the host name for the server */
private int mPort;
private String mPath; /* Stores the path for the server */
private String mAuthPath; /* Stores the path off of the server to post data to for form based authentication */
private String mMailboxPath; /* Stores the user specified path to the mailbox */
private WebDavHttpClient mHttpClient = null;
private HttpContext mContext = null;
private String mAuthString;
private CookieStore mAuthCookies = null;
private short mAuthentication = AUTH_TYPE_NONE;
private String mCachedLoginUrl;
private Folder mSendFolder = null;
private Map mFolderList = new HashMap();
public WebDavStore(StoreConfig storeConfig) throws MessagingException {
super(storeConfig);
WebDavStoreSettings settings;
try {
settings = decodeUri(storeConfig.getStoreUri());
} catch (IllegalArgumentException e) {
throw new MessagingException("Error while decoding store URI", e);
}
mHost = settings.host;
mPort = settings.port;
mConnectionSecurity = settings.connectionSecurity;
mUsername = settings.username;
mPassword = settings.password;
mAlias = settings.alias;
mPath = settings.path;
mAuthPath = settings.authPath;
mMailboxPath = settings.mailboxPath;
if (mPath == null || mPath.equals("")) {
mPath = "/Exchange";
} else if (!mPath.startsWith("/")) {
mPath = "/" + mPath;
}
if (mMailboxPath == null || mMailboxPath.equals("")) {
mMailboxPath = "/" + mAlias;
} else if (!mMailboxPath.startsWith("/")) {
mMailboxPath = "/" + mMailboxPath;
}
if (mAuthPath != null &&
!mAuthPath.equals("") &&
!mAuthPath.startsWith("/")) {
mAuthPath = "/" + mAuthPath;
}
// The URL typically looks like the following: "https://mail.domain.com/Exchange/alias".
// The inbox path would look like: "https://mail.domain.com/Exchange/alias/Inbox".
mUrl = getRoot() + mPath + mMailboxPath;
mAuthString = "Basic " + Utility.base64Encode(mUsername + ":" + mPassword);
}
private String getRoot() {
String root;
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
root = "https";
} else {
root = "http";
}
root += "://" + mHost + ":" + mPort;
return root;
}
@Override
public void checkSettings() throws MessagingException {
authenticate();
}
@Override
public List extends Folder > getPersonalNamespaces(boolean forceListAll) throws MessagingException {
List folderList = new LinkedList();
/**
* We have to check authentication here so we have the proper URL stored
*/
getHttpClient();
/**
* Firstly we get the "special" folders list (inbox, outbox, etc)
* and setup the account accordingly
*/
Map headers = new HashMap();
DataSet dataset = new DataSet();
headers.put("Depth", "0");
headers.put("Brief", "t");
dataset = processRequest(this.mUrl, "PROPFIND", getSpecialFoldersList(), headers);
Map specialFoldersMap = dataset.getSpecialFolderToUrl();
String folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_INBOX_FOLDER));
if (folderName != null) {
mStoreConfig.setAutoExpandFolderName(folderName);
mStoreConfig.setInboxFolderName(folderName);
}
folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_DRAFTS_FOLDER));
if (folderName != null)
mStoreConfig.setDraftsFolderName(folderName);
folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_TRASH_FOLDER));
if (folderName != null)
mStoreConfig.setTrashFolderName(folderName);
folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_SPAM_FOLDER));
if (folderName != null)
mStoreConfig.setSpamFolderName(folderName);
// K-9 Mail's outbox is a special local folder and different from Exchange/WebDAV's outbox.
/*
folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_OUTBOX_FOLDER));
if (folderName != null)
mAccount.setOutboxFolderName(folderName);
*/
folderName = getFolderName(specialFoldersMap.get(DAV_MAIL_SENT_FOLDER));
if (folderName != null)
mStoreConfig.setSentFolderName(folderName);
/**
* Next we get all the folders (including "special" ones)
*/
headers = new HashMap();
dataset = new DataSet();
headers.put("Brief", "t");
dataset = processRequest(this.mUrl, "SEARCH", getFolderListXml(), headers);
String[] folderUrls = dataset.getHrefs();
for (String tempUrl : folderUrls) {
WebDavFolder folder = createFolder(tempUrl);
if (folder != null)
folderList.add(folder);
}
return folderList;
}
/**
* Creates a folder using the URL passed as parameter (only if it has not been
* already created) and adds this to our store folder map.
*
* @param folderUrl
* @return
*/
private WebDavFolder createFolder(String folderUrl) {
if (folderUrl == null)
return null;
WebDavFolder wdFolder = null;
String folderName = getFolderName(folderUrl);
if (folderName != null) {
if (!this.mFolderList.containsKey(folderName)) {
wdFolder = new WebDavFolder(this, folderName);
wdFolder.setUrl(folderUrl);
mFolderList.put(folderName, wdFolder);
}
}
// else: Unknown URL format => NO Folder created
return wdFolder;
}
private String getFolderName(String folderUrl) {
if (folderUrl == null)
return null;
// Here we extract the folder name starting from the complete url.
// folderUrl is in the form http://mail.domain.com/exchange/username/foldername
// so we need "foldername" which is the string after the fifth slash
int folderSlash = -1;
for (int j = 0; j < 5; j++) {
folderSlash = folderUrl.indexOf('/', folderSlash + 1);
if (folderSlash < 0)
break;
}
if (folderSlash > 0) {
String fullPathName;
// Removes the final slash if present
if (folderUrl.charAt(folderUrl.length() - 1) == '/')
fullPathName = folderUrl.substring(folderSlash + 1, folderUrl.length() - 1);
else
fullPathName = folderUrl.substring(folderSlash + 1);
// Decodes the url-encoded folder name (i.e. "My%20folder" => "My Folder"
return UrlEncodingHelper.decodeUtf8(fullPathName);
}
return null;
}
@Override
public Folder getFolder(String name) {
WebDavFolder folder;
if ((folder = this.mFolderList.get(name)) == null) {
folder = new WebDavFolder(this, name);
}
return folder;
}
public Folder getSendSpoolFolder() throws MessagingException {
if (mSendFolder == null)
mSendFolder = getFolder(DAV_MAIL_SEND_FOLDER);
return mSendFolder;
}
@Override
public boolean isMoveCapable() {
return true;
}
@Override
public boolean isCopyCapable() {
return true;
}
private String getSpecialFoldersList() {
StringBuilder buffer = new StringBuilder(200);
buffer.append("");
buffer.append("");
buffer.append("");
buffer.append("<").append(DAV_MAIL_INBOX_FOLDER).append(" xmlns=\"urn:schemas:httpmail:\"/>");
buffer.append("<").append(DAV_MAIL_DRAFTS_FOLDER).append(" xmlns=\"urn:schemas:httpmail:\"/>");
buffer.append("<").append(DAV_MAIL_OUTBOX_FOLDER).append(" xmlns=\"urn:schemas:httpmail:\"/>");
buffer.append("<").append(DAV_MAIL_SENT_FOLDER).append(" xmlns=\"urn:schemas:httpmail:\"/>");
buffer.append("<").append(DAV_MAIL_TRASH_FOLDER).append(" xmlns=\"urn:schemas:httpmail:\"/>");
// This should always be ##DavMailSubmissionURI## for which we already have a constant
// buffer.append("");
buffer.append("<").append(DAV_MAIL_SPAM_FOLDER).append(" xmlns=\"urn:schemas:httpmail:\"/>");
buffer.append("");
buffer.append("");
return buffer.toString();
}
/***************************************************************
* WebDAV XML Request body retrieval functions
*/
private String getFolderListXml() {
StringBuilder buffer = new StringBuilder(200);
buffer.append("");
buffer.append("\r\n");
buffer.append("SELECT \"DAV:uid\", \"DAV:ishidden\"\r\n");
buffer.append(" FROM SCOPE('hierarchical traversal of \"").append(this.mUrl).append("\"')\r\n");
buffer.append(" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=True\r\n");
buffer.append("\r\n");
return buffer.toString();
}
private String getMessageCountXml(String messageState) {
StringBuilder buffer = new StringBuilder(200);
buffer.append("");
buffer.append("\r\n");
buffer.append("SELECT \"DAV:visiblecount\"\r\n");
buffer.append(" FROM \"\"\r\n");
buffer.append(" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=False AND \"urn:schemas:httpmail:read\"=")
.append(messageState).append("\r\n");
buffer.append(" GROUP BY \"DAV:ishidden\"\r\n");
buffer.append("\r\n");
return buffer.toString();
}
private String getMessageEnvelopeXml(String[] uids) {
StringBuilder buffer = new StringBuilder(200);
buffer.append("");
buffer.append("\r\n");
buffer.append("SELECT \"DAV:uid\", \"DAV:getcontentlength\",");
buffer.append(" \"urn:schemas:mailheader:mime-version\",");
buffer.append(" \"urn:schemas:mailheader:content-type\",");
buffer.append(" \"urn:schemas:mailheader:subject\",");
buffer.append(" \"urn:schemas:mailheader:date\",");
buffer.append(" \"urn:schemas:mailheader:thread-topic\",");
buffer.append(" \"urn:schemas:mailheader:thread-index\",");
buffer.append(" \"urn:schemas:mailheader:from\",");
buffer.append(" \"urn:schemas:mailheader:to\",");
buffer.append(" \"urn:schemas:mailheader:in-reply-to\",");
buffer.append(" \"urn:schemas:mailheader:cc\",");
buffer.append(" \"urn:schemas:httpmail:read\"");
buffer.append(" \r\n");
buffer.append(" FROM \"\"\r\n");
buffer.append(" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=False AND ");
for (int i = 0, count = uids.length; i < count; i++) {
if (i != 0) {
buffer.append(" OR ");
}
buffer.append(" \"DAV:uid\"='").append(uids[i]).append("' ");
}
buffer.append("\r\n");
buffer.append("\r\n");
return buffer.toString();
}
private String getMessagesXml() {
StringBuilder buffer = new StringBuilder(200);
buffer.append("");
buffer.append("\r\n");
buffer.append("SELECT \"DAV:uid\"\r\n");
buffer.append(" FROM \"\"\r\n");
buffer.append(" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=False\r\n");
buffer.append("\r\n");
return buffer.toString();
}
private String getMessageUrlsXml(String[] uids) {
StringBuilder buffer = new StringBuilder(600);
buffer.append("");
buffer.append("\r\n");
buffer.append("SELECT \"urn:schemas:httpmail:read\", \"DAV:uid\"\r\n");
buffer.append(" FROM \"\"\r\n");
buffer.append(" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=False AND ");
for (int i = 0, count = uids.length; i < count; i++) {
if (i != 0) {
buffer.append(" OR ");
}
buffer.append(" \"DAV:uid\"='").append(uids[i]).append("' ");
}
buffer.append("\r\n");
buffer.append("\r\n");
return buffer.toString();
}
private String getMessageFlagsXml(String[] uids) throws MessagingException {
if (uids.length == 0) {
throw new MessagingException("Attempt to get flags on 0 length array for uids");
}
StringBuilder buffer = new StringBuilder(200);
buffer.append("");
buffer.append("\r\n");
buffer.append("SELECT \"urn:schemas:httpmail:read\", \"DAV:uid\"\r\n");
buffer.append(" FROM \"\"\r\n");
buffer.append(" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=False AND ");
for (int i = 0, count = uids.length; i < count; i++) {
if (i != 0) {
buffer.append(" OR ");
}
buffer.append(" \"DAV:uid\"='").append(uids[i]).append("' ");
}
buffer.append("\r\n");
buffer.append("\r\n");
return buffer.toString();
}
private String getMarkMessagesReadXml(String[] urls, boolean read) {
StringBuilder buffer = new StringBuilder(600);
buffer.append("\r\n");
buffer.append("\r\n");
buffer.append("\r\n");
for (String url : urls) {
buffer.append(" ").append(url).append("\r\n");
}
buffer.append("\r\n");
buffer.append("\r\n");
buffer.append(" \r\n");
buffer.append(" ").append(read ? "1" : "0").append("\r\n");
buffer.append(" \r\n");
buffer.append("\r\n");
buffer.append("\r\n");
return buffer.toString();
}
// For flag:
// http://www.devnewsgroups.net/group/microsoft.public.exchange.development/topic27175.aspx
// "1" & _
private String getMoveOrCopyMessagesReadXml(String[] urls, boolean isMove) {
String action = (isMove ? "move" : "copy");
StringBuilder buffer = new StringBuilder(600);
buffer.append("\r\n");
buffer.append("\r\n");
buffer.append("\r\n");
for (String url : urls) {
buffer.append(" ").append(url).append("\r\n");
}
buffer.append("\r\n");
buffer.append("\r\n");
return buffer.toString();
}
/***************************************************************
* Authentication related methods
*/
/**
* Determines which type of authentication Exchange is using and authenticates appropriately.
*
* @throws MessagingException
*/
public boolean authenticate()
throws MessagingException {
try {
if (mAuthentication == AUTH_TYPE_NONE) {
ConnectionInfo info = doInitialConnection();
if (info.requiredAuthType == AUTH_TYPE_BASIC) {
HttpGeneric request = new HttpGeneric(mUrl);
request.setMethod("GET");
request.setHeader("Authorization", mAuthString);
WebDavHttpClient httpClient = new WebDavHttpClient();
HttpResponse response = httpClient.executeOverride(request, mContext);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 200 && statusCode < 300) {
mAuthentication = AUTH_TYPE_BASIC;
} else if (statusCode == 401) {
throw new MessagingException("Invalid username or password for authentication.");
} else {
throw new MessagingException("Error with code " + response.getStatusLine().getStatusCode() +
" during request processing: " + response.getStatusLine().toString());
}
} else if (info.requiredAuthType == AUTH_TYPE_FORM_BASED) {
doFBA(info);
}
} else if (mAuthentication == AUTH_TYPE_BASIC) {
// Nothing to do, we authenticate with every request when
// using basic authentication.
} else if (mAuthentication == AUTH_TYPE_FORM_BASED) {
// Our cookie expired, re-authenticate.
doFBA(null);
}
} catch (IOException ioe) {
Log.e(K9.LOG_TAG, "Error during authentication: " + ioe + "\nStack: " + processException(ioe));
throw new MessagingException("Error during authentication", ioe);
}
return mAuthentication != AUTH_TYPE_NONE;
}
/**
* Makes the initial connection to Exchange for authentication. Determines the type of authentication necessary for
* the server.
*
* @throws MessagingException
*/
private ConnectionInfo doInitialConnection()
throws MessagingException {
// For our initial connection we are sending an empty GET request to
// the configured URL, which should be in the following form:
// https://mail.server.com/Exchange/alias
//
// Possible status codes include:
// 401 - the server uses basic authentication
// 30x - the server is trying to redirect us to an OWA login
// 20x - success
//
// The latter two indicate form-based authentication.
ConnectionInfo info = new ConnectionInfo();
WebDavHttpClient httpClient = getHttpClient();
HttpGeneric request = new HttpGeneric(mUrl);
request.setMethod("GET");
try {
HttpResponse response = httpClient.executeOverride(request, mContext);
info.statusCode = response.getStatusLine().getStatusCode();
if (info.statusCode == 401) {
// 401 is the "Unauthorized" status code, meaning the server wants
// an authentication header for basic authentication.
info.requiredAuthType = AUTH_TYPE_BASIC;
} else if ((info.statusCode >= 200 && info.statusCode < 300) || // Success
(info.statusCode >= 300 && info.statusCode < 400) || // Redirect
(info.statusCode == 440)) { // Unauthorized
// We will handle all 3 situations the same. First we take an educated
// guess at where the authorization DLL is located. If this is this
// doesn't work, then we'll use the redirection URL for OWA login given
// to us by exchange. We can use this to scrape the location of the
// authorization URL.
info.requiredAuthType = AUTH_TYPE_FORM_BASED;
if (mAuthPath != null && !mAuthPath.equals("")) {
// The user specified their own authentication path, use that.
info.guessedAuthUrl = getRoot() + mAuthPath;
} else {
// Use the default path to the authentication dll.
info.guessedAuthUrl = getRoot() + "/exchweb/bin/auth/owaauth.dll";
}
// Determine where the server is trying to redirect us.
Header location = response.getFirstHeader("Location");
if (location != null) {
info.redirectUrl = location.getValue();
}
} else {
throw new IOException("Error with code " + info.statusCode + " during request processing: " +
response.getStatusLine().toString());
}
} catch (SSLException e) {
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
Log.e(K9.LOG_TAG, "IOException: " + ioe + "\nTrace: " + processException(ioe));
throw new MessagingException("IOException", ioe);
}
return info;
}
/**
* Performs form-based authentication.
*
* @throws MessagingException
*/
public void doFBA(ConnectionInfo info)
throws IOException, MessagingException {
// Clear out cookies from any previous authentication.
mAuthCookies.clear();
WebDavHttpClient httpClient = getHttpClient();
String loginUrl;
if (info != null) {
loginUrl = info.guessedAuthUrl;
} else if (mCachedLoginUrl != null && !mCachedLoginUrl.equals("")) {
loginUrl = mCachedLoginUrl;
} else {
throw new MessagingException("No valid login URL available for form-based authentication.");
}
HttpGeneric request = new HttpGeneric(loginUrl);
request.setMethod("POST");
// Build the POST data.
List pairs = new ArrayList();
pairs.add(new BasicNameValuePair("destination", mUrl));
pairs.add(new BasicNameValuePair("username", mUsername));
pairs.add(new BasicNameValuePair("password", mPassword));
pairs.add(new BasicNameValuePair("flags", "0"));
pairs.add(new BasicNameValuePair("SubmitCreds", "Log+On"));
pairs.add(new BasicNameValuePair("forcedownlevel", "0"));
pairs.add(new BasicNameValuePair("trusted", "0"));
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(pairs);
request.setEntity(formEntity);
HttpResponse response = httpClient.executeOverride(request, mContext);
boolean authenticated = testAuthenticationResponse(response);
if (!authenticated) {
// Check the response from the authentication request above for a form action.
String formAction = findFormAction(WebDavHttpClient.getUngzippedContent(response.getEntity()));
if (formAction == null) {
// If there is no form action, try using our redirect URL from the initial connection.
if (info != null && info.redirectUrl != null && !info.redirectUrl.equals("")) {
loginUrl = info.redirectUrl;
request = new HttpGeneric(loginUrl);
request.setMethod("GET");
response = httpClient.executeOverride(request, mContext);
formAction = findFormAction(WebDavHttpClient.getUngzippedContent(response.getEntity()));
}
}
if (formAction != null) {
try {
URI formActionUri = new URI(formAction);
URI loginUri = new URI(loginUrl);
if (formActionUri.isAbsolute()) {
// The form action is an absolute URL, just use it.
loginUrl = formAction;
} else {
// Append the form action to our current URL, minus the file name.
String urlPath;
if (formAction.startsWith("/")) {
urlPath = formAction;
} else {
urlPath = loginUri.getPath();
int lastPathPos = urlPath.lastIndexOf('/');
if (lastPathPos > -1) {
urlPath = urlPath.substring(0, lastPathPos + 1);
urlPath = urlPath.concat(formAction);
}
}
// Reconstruct the login URL based on the original login URL and the form action.
URI finalUri = new URI(loginUri.getScheme(),
loginUri.getUserInfo(),
loginUri.getHost(),
loginUri.getPort(),
urlPath,
null,
null);
loginUrl = finalUri.toString();
}
// Retry the login using our new URL.
request = new HttpGeneric(loginUrl);
request.setMethod("POST");
request.setEntity(formEntity);
response = httpClient.executeOverride(request, mContext);
authenticated = testAuthenticationResponse(response);
} catch (URISyntaxException e) {
Log.e(K9.LOG_TAG, "URISyntaxException caught " + e + "\nTrace: " + processException(e));
throw new MessagingException("URISyntaxException caught", e);
}
} else {
throw new MessagingException("A valid URL for Exchange authentication could not be found.");
}
}
if (authenticated) {
mAuthentication = AUTH_TYPE_FORM_BASED;
mCachedLoginUrl = loginUrl;
} else {
throw new MessagingException("Invalid credentials provided for authentication.");
}
}
/**
* Searches the specified stream for an HTML form and returns the form's action target.
*
* @throws IOException
*/
private String findFormAction(InputStream istream)
throws IOException {
String formAction = null;
BufferedReader reader = new BufferedReader(new InputStreamReader(istream), 4096);
String tempText;
// Read line by line until we find something like: