package com.fsck.k9.mail.store.webdav;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.store.RemoteStore;
import com.fsck.k9.mail.store.StoreConfig;
import javax.net.ssl.SSLException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
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.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import timber.log.Timber;
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_WEBDAV;
import static com.fsck.k9.mail.helper.UrlEncodingHelper.decodeUtf8;
/**
* <pre>
* Uses WebDAV formatted HTTP calls to an MS Exchange server to fetch email
* and email information.
* </pre>
*/
@SuppressWarnings("deprecation")
public class WebDavStore extends RemoteStore {
public static WebDavStoreSettings decodeUri(String uri) {
return WebDavStoreUriDecoder.decode(uri);
}
public static String createUri(ServerSettings server) {
return WebDavStoreUriCreator.create(server);
}
private ConnectionSecurity mConnectionSecurity;
private String username;
private String alias;
private String password;
private String baseUrl;
private String hostname;
private int port;
private String path;
private String formBasedAuthPath;
private String mailboxPath;
private final WebDavHttpClient.WebDavHttpClientFactory httpClientFactory;
private WebDavHttpClient httpClient = null;
private HttpContext httpContext = null;
private String authString;
private CookieStore authCookies = null;
private short authenticationType = WebDavConstants.AUTH_TYPE_NONE;
private String cachedLoginUrl;
private Folder sendFolder = null;
private Map<String, WebDavFolder> folderList = new HashMap<>();
public WebDavStore(StoreConfig storeConfig, WebDavHttpClient.WebDavHttpClientFactory clientFactory)
throws MessagingException {
super(storeConfig, null);
httpClientFactory = clientFactory;
WebDavStoreSettings settings;
try {
settings = WebDavStore.decodeUri(storeConfig.getStoreUri());
} catch (IllegalArgumentException e) {
throw new MessagingException("Error while decoding store URI", e);
}
hostname = settings.host;
port = settings.port;
mConnectionSecurity = settings.connectionSecurity;
username = settings.username;
password = settings.password;
alias = settings.alias;
path = settings.path;
formBasedAuthPath = settings.authPath;
mailboxPath = settings.mailboxPath;
if (path == null || path.equals("")) {
path = "/Exchange";
} else if (!path.startsWith("/")) {
path = "/" + path;
}
if (mailboxPath == null || mailboxPath.equals("")) {
mailboxPath = "/" + alias;
} else if (!mailboxPath.startsWith("/")) {
mailboxPath = "/" + mailboxPath;
}
if (formBasedAuthPath != null &&
!formBasedAuthPath.equals("") &&
!formBasedAuthPath.startsWith("/")) {
formBasedAuthPath = "/" + formBasedAuthPath;
}
// 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".
baseUrl = getRoot() + path + mailboxPath;
authString = "Basic " + Base64.encode(username + ":" + password);
}
private String getRoot() {
String root;
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
root = "https";
} else {
root = "http";
}
root += "://" + hostname + ":" + port;
return root;
}
HttpContext getHttpContext() {
return httpContext;
}
short getAuthentication() {
return authenticationType;
}
StoreConfig getStoreConfig() {
return mStoreConfig;
}
@Override
public void checkSettings() throws MessagingException {
authenticate();
}
@Override
public List<? extends Folder> getPersonalNamespaces(boolean forceListAll) throws MessagingException {
List<Folder> 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<String, String> headers = new HashMap<>();
headers.put("Depth", "0");
headers.put("Brief", "t");
DataSet dataset = processRequest(this.baseUrl, "PROPFIND", getSpecialFoldersList(), headers);
Map<String, String> specialFoldersMap = dataset.getSpecialFolderToUrl();
String folderName = getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_INBOX_FOLDER));
if (folderName != null) {
mStoreConfig.setAutoExpandFolderName(folderName);
mStoreConfig.setInboxFolderName(folderName);
}
folderName = getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_DRAFTS_FOLDER));
if (folderName != null) {
mStoreConfig.setDraftsFolderName(folderName);
}
folderName = getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_TRASH_FOLDER));
if (folderName != null) {
mStoreConfig.setTrashFolderName(folderName);
}
folderName = getFolderName(specialFoldersMap.get(WebDavConstants.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(WebDavConstants.DAV_MAIL_SENT_FOLDER));
if (folderName != null) {
mStoreConfig.setSentFolderName(folderName);
}
/*
* Next we get all the folders (including "special" ones)
*/
headers = new HashMap<>();
headers.put("Brief", "t");
dataset = processRequest(this.baseUrl, "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
* URL
*
* @return WebDAV remote folder
*/
private WebDavFolder createFolder(String folderUrl) {
if (folderUrl == null) {
return null;
}
WebDavFolder wdFolder = null;
String folderName = getFolderName(folderUrl);
if (folderName != null) {
wdFolder = getFolder(folderName);
if (wdFolder != null) {
wdFolder.setUrl(folderUrl);
}
}
// 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 decodeUtf8(fullPathName);
}
return null;
}
@Override
public WebDavFolder getFolder(String name) {
WebDavFolder folder = this.folderList.get(name);
if (folder == null) {
folder = new WebDavFolder(this, name);
folderList.put(name, folder);
}
return folder;
}
private Folder getSendSpoolFolder() throws MessagingException {
if (sendFolder == null) {
sendFolder = getFolder(WebDavConstants.DAV_MAIL_SEND_FOLDER);
}
return sendFolder;
}
@Override
public boolean isMoveCapable() {
return true;
}
@Override
public boolean isCopyCapable() {
return true;
}
private String getSpecialFoldersList() {
return "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>" +
"<propfind xmlns=\"DAV:\">" +
"<prop>" +
"<" + WebDavConstants.DAV_MAIL_INBOX_FOLDER + " xmlns=\"urn:schemas:httpmail:\"/>" +
"<" + WebDavConstants.DAV_MAIL_DRAFTS_FOLDER + " xmlns=\"urn:schemas:httpmail:\"/>" +
"<" + WebDavConstants.DAV_MAIL_OUTBOX_FOLDER + " xmlns=\"urn:schemas:httpmail:\"/>" +
"<" + WebDavConstants.DAV_MAIL_SENT_FOLDER + " xmlns=\"urn:schemas:httpmail:\"/>" +
"<" + WebDavConstants.DAV_MAIL_TRASH_FOLDER + " xmlns=\"urn:schemas:httpmail:\"/>" +
"<" + WebDavConstants.DAV_MAIL_SPAM_FOLDER + " xmlns=\"urn:schemas:httpmail:\"/>" +
// This should always be ##DavMailSubmissionURI## for which we already have a constant
// "<sendmsg xmlns=\"urn:schemas:httpmail:\"/>" +
"</prop>" +
"</propfind>";
}
/***************************************************************
* WebDAV XML Request body retrieval functions
*/
private String getFolderListXml() {
return "<?xml version='1.0' ?>" +
"<a:searchrequest xmlns:a='DAV:'><a:sql>\r\n" +
"SELECT \"DAV:uid\", \"DAV:ishidden\"\r\n" +
" FROM SCOPE('deep traversal of \"" + this.baseUrl + "\"')\r\n" +
" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=True\r\n" +
"</a:sql></a:searchrequest>\r\n";
}
String getMessageCountXml(String messageState) {
return "<?xml version='1.0' ?>" +
"<a:searchrequest xmlns:a='DAV:'><a:sql>\r\n" +
"SELECT \"DAV:visiblecount\"\r\n" +
" FROM \"\"\r\n" +
" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=False AND \"urn:schemas:httpmail:read\"=" +
messageState + "\r\n" +
" GROUP BY \"DAV:ishidden\"\r\n" +
"</a:sql></a:searchrequest>\r\n";
}
String getMessageEnvelopeXml(String[] uids) {
StringBuilder buffer = new StringBuilder(200);
buffer.append("<?xml version='1.0' ?>");
buffer.append("<a:searchrequest xmlns:a='DAV:'><a:sql>\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("</a:sql></a:searchrequest>\r\n");
return buffer.toString();
}
String getMessagesXml() {
return "<?xml version='1.0' ?>" +
"<a:searchrequest xmlns:a='DAV:'><a:sql>\r\n" +
"SELECT \"DAV:uid\"\r\n" +
" FROM \"\"\r\n" +
" WHERE \"DAV:ishidden\"=False AND \"DAV:isfolder\"=False\r\n" +
"</a:sql></a:searchrequest>\r\n";
}
String getMessageUrlsXml(String[] uids) {
StringBuilder buffer = new StringBuilder(600);
buffer.append("<?xml version='1.0' ?>");
buffer.append("<a:searchrequest xmlns:a='DAV:'><a:sql>\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("</a:sql></a:searchrequest>\r\n");
return buffer.toString();
}
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("<?xml version='1.0' ?>");
buffer.append("<a:searchrequest xmlns:a='DAV:'><a:sql>\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("</a:sql></a:searchrequest>\r\n");
return buffer.toString();
}
String getMarkMessagesReadXml(String[] urls, boolean read) {
StringBuilder buffer = new StringBuilder(600);
buffer.append("<?xml version='1.0' ?>\r\n");
buffer.append("<a:propertyupdate xmlns:a='DAV:' xmlns:b='urn:schemas:httpmail:'>\r\n");
buffer.append("<a:target>\r\n");
for (String url : urls) {
buffer.append(" <a:href>").append(url).append("</a:href>\r\n");
}
buffer.append("</a:target>\r\n");
buffer.append("<a:set>\r\n");
buffer.append(" <a:prop>\r\n");
buffer.append(" <b:read>").append(read ? "1" : "0").append("</b:read>\r\n");
buffer.append(" </a:prop>\r\n");
buffer.append("</a:set>\r\n");
buffer.append("</a:propertyupdate>\r\n");
return buffer.toString();
}
// For flag:
// http://www.devnewsgroups.net/group/microsoft.public.exchange.development/topic27175.aspx
// "<m:0x10900003>1</m:0x10900003>" & _
String getMoveOrCopyMessagesReadXml(String[] urls, boolean isMove) {
String action = (isMove ? "move" : "copy");
StringBuilder buffer = new StringBuilder(600);
buffer.append("<?xml version='1.0' ?>\r\n");
buffer.append("<a:").append(action).append(" xmlns:a='DAV:' xmlns:b='urn:schemas:httpmail:'>\r\n");
buffer.append("<a:target>\r\n");
for (String url : urls) {
buffer.append(" <a:href>").append(url).append("</a:href>\r\n");
}
buffer.append("</a:target>\r\n");
buffer.append("</a:").append(action).append(">\r\n");
return buffer.toString();
}
private boolean authenticate()
throws MessagingException {
try {
if (authenticationType == WebDavConstants.AUTH_TYPE_NONE) {
ConnectionInfo info = doInitialConnection();
if (info.requiredAuthType == WebDavConstants.AUTH_TYPE_BASIC) {
HttpGeneric request = new HttpGeneric(baseUrl);
request.setMethod("GET");
request.setHeader("Authorization", authString);
WebDavHttpClient httpClient = getHttpClient();
HttpResponse response = httpClient.executeOverride(request, httpContext);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 200 && statusCode < 300) {
authenticationType = WebDavConstants.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 == WebDavConstants.AUTH_TYPE_FORM_BASED) {
performFormBasedAuthentication(info);
}
} else if (authenticationType == WebDavConstants.AUTH_TYPE_BASIC) {
// Nothing to do, we authenticate with every request when
// using basic authentication.
} else if (authenticationType == WebDavConstants.AUTH_TYPE_FORM_BASED) {
// Our cookie expired, re-authenticate.
performFormBasedAuthentication(null);
}
} catch (IOException ioe) {
Timber.e(ioe, "Error during authentication");
throw new MessagingException("Error during authentication", ioe);
}
return authenticationType != WebDavConstants.AUTH_TYPE_NONE;
}
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(baseUrl);
request.setMethod("GET");
try {
HttpResponse response = httpClient.executeOverride(request, httpContext);
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 = WebDavConstants.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 = WebDavConstants.AUTH_TYPE_FORM_BASED;
if (formBasedAuthPath != null && !formBasedAuthPath.equals("")) {
// The user specified their own authentication path, use that.
info.guessedAuthUrl = getRoot() + formBasedAuthPath;
} 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) {
Timber.e(ioe, "IOException during initial connection");
throw new MessagingException("IOException", ioe);
}
return info;
}
private void performFormBasedAuthentication(ConnectionInfo info)
throws IOException, MessagingException {
// Clear out cookies from any previous authentication.
if (authCookies != null) {
authCookies.clear();
}
WebDavHttpClient httpClient = getHttpClient();
String loginUrl;
if (info != null) {
loginUrl = info.guessedAuthUrl;
} else if (cachedLoginUrl != null && !cachedLoginUrl.equals("")) {
loginUrl = cachedLoginUrl;
} 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<BasicNameValuePair> pairs = new ArrayList<>();
pairs.add(new BasicNameValuePair("destination", baseUrl));
pairs.add(new BasicNameValuePair("username", username));
pairs.add(new BasicNameValuePair("password", password));
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, httpContext);
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, httpContext);
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, httpContext);
authenticated = testAuthenticationResponse(response);
} catch (URISyntaxException e) {
Timber.e(e, "URISyntaxException caught");
throw new MessagingException("URISyntaxException caught", e);
}
} else {
throw new MessagingException("A valid URL for Exchange authentication could not be found.");
}
}
if (authenticated) {
authenticationType = WebDavConstants.AUTH_TYPE_FORM_BASED;
cachedLoginUrl = loginUrl;
} else {
throw new MessagingException("Invalid credentials provided for authentication.");
}
}
private String findFormAction(InputStream istream)
throws IOException {
String formAction = null;
BufferedReader reader = new BufferedReader(new InputStreamReader(istream), 4096);
String tempText;
//TODO: Use proper HTML parsing for this
// Read line by line until we find something like: <form action="owaauth.dll"...>.
tempText = reader.readLine();
while (formAction == null) {
if (tempText.contains(" action=")) {
String[] actionParts = tempText.split(" action=");
if (actionParts.length > 1 && actionParts[1].length() > 1) {
char openQuote = actionParts[1].charAt(0);
int closePos = actionParts[1].indexOf(openQuote, 1);
if (closePos > 1) {
formAction = actionParts[1].substring(1, closePos);
// Remove any GET parameters.
int quesPos = formAction.indexOf('?');
if (quesPos != -1) {
formAction = formAction.substring(0, quesPos);
}
}
}
}
tempText = reader.readLine();
}
return formAction;
}
private boolean testAuthenticationResponse(HttpResponse response)
throws MessagingException {
boolean authenticated = false;
int statusCode = response.getStatusLine().getStatusCode();
// Exchange 2007 will return a 302 status code no matter what.
if (((statusCode >= 200 && statusCode < 300) || statusCode == 302) &&
authCookies != null && !authCookies.getCookies().isEmpty()) {
// We may be authenticated, we need to send a test request to know for sure.
// Exchange 2007 adds the same cookies whether the username and password were valid or not.
ConnectionInfo info = doInitialConnection();
if (info.statusCode >= 200 && info.statusCode < 300) {
authenticated = true;
} else if (info.statusCode == 302) {
// If we are successfully authenticated, Exchange will try to redirect us to our OWA inbox.
// Otherwise, it will redirect us to a logon page.
// Our URL is in the form: https://hostname:port/Exchange/alias.
// The redirect is in the form: https://hostname:port/owa/alias.
// Do a simple replace and compare the resulting strings.
try {
String thisPath = new URI(baseUrl).getPath();
String redirectPath = new URI(info.redirectUrl).getPath();
if (!thisPath.endsWith("/")) {
thisPath = thisPath.concat("/");
}
if (!redirectPath.endsWith("/")) {
redirectPath = redirectPath.concat("/");
}
if (redirectPath.equalsIgnoreCase(thisPath)) {
authenticated = true;
} else {
int found = thisPath.indexOf('/', 1);
if (found != -1) {
String replace = thisPath.substring(0, found + 1);
redirectPath = redirectPath.replace("/owa/", replace);
if (redirectPath.equalsIgnoreCase(thisPath)) {
authenticated = true;
}
}
}
} catch (URISyntaxException e) {
Timber.e(e, "URISyntaxException");
throw new MessagingException("URISyntaxException caught", e);
}
}
}
return authenticated;
}
public CookieStore getAuthCookies() {
return authCookies;
}
public String getAlias() {
return alias;
}
public String getUrl() {
return baseUrl;
}
public WebDavHttpClient getHttpClient() throws MessagingException {
if (httpClient == null) {
httpClient = httpClientFactory.create();
// Disable automatic redirects on the http client.
httpClient.getParams().setBooleanParameter("http.protocol.handle-redirects", false);
// Setup a cookie store for forms-based authentication.
httpContext = new BasicHttpContext();
authCookies = new BasicCookieStore();
httpContext.setAttribute(ClientContext.COOKIE_STORE, authCookies);
SchemeRegistry reg = httpClient.getConnectionManager().getSchemeRegistry();
try {
Scheme s = new Scheme("https", new WebDavSocketFactory(hostname, 443), 443);
reg.register(s);
} catch (NoSuchAlgorithmException nsa) {
Timber.e(nsa, "NoSuchAlgorithmException in getHttpClient");
throw new MessagingException("NoSuchAlgorithmException in getHttpClient: ", nsa);
} catch (KeyManagementException kme) {
Timber.e(kme, "KeyManagementException in getHttpClient");
throw new MessagingException("KeyManagementException in getHttpClient: ", kme);
}
}
return httpClient;
}
protected InputStream sendRequest(String url, String method, StringEntity messageBody,
Map<String, String> headers, boolean tryAuth)
throws MessagingException {
if (url == null || method == null) {
return null;
}
WebDavHttpClient httpClient = getHttpClient();
try {
int statusCode;
HttpGeneric httpMethod = new HttpGeneric(url);
HttpResponse response;
HttpEntity entity;
if (messageBody != null) {
httpMethod.setEntity(messageBody);
}
if (headers != null) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
httpMethod.setHeader(entry.getKey(), entry.getValue());
}
}
if (authenticationType == WebDavConstants.AUTH_TYPE_NONE) {
if (!tryAuth || !authenticate()) {
throw new MessagingException("Unable to authenticate in sendRequest().");
}
} else if (authenticationType == WebDavConstants.AUTH_TYPE_BASIC) {
httpMethod.setHeader("Authorization", authString);
}
httpMethod.setMethod(method);
response = httpClient.executeOverride(httpMethod, httpContext);
statusCode = response.getStatusLine().getStatusCode();
entity = response.getEntity();
if (statusCode == 401) {
throw new MessagingException("Invalid username or password for Basic authentication.");
} else if (statusCode == 440) {
if (tryAuth && authenticationType == WebDavConstants.AUTH_TYPE_FORM_BASED) {
// Our cookie expired, re-authenticate.
performFormBasedAuthentication(null);
sendRequest(url, method, messageBody, headers, false);
} else {
throw new MessagingException("Authentication failure in sendRequest().");
}
} else if (statusCode == 302) {
handleUnexpectedRedirect(response, url);
} else if (statusCode < 200 || statusCode >= 300) {
throw new IOException("Error with code " + statusCode + " during request processing: " +
response.getStatusLine().toString());
}
if (entity != null) {
return WebDavHttpClient.getUngzippedContent(entity);
}
} catch (UnsupportedEncodingException uee) {
Timber.e(uee, "UnsupportedEncodingException: ");
throw new MessagingException("UnsupportedEncodingException", uee);
} catch (IOException ioe) {
Timber.e(ioe, "IOException: ");
throw new MessagingException("IOException", ioe);
}
return null;
}
private void handleUnexpectedRedirect(HttpResponse response, String url) throws IOException {
if (response.getFirstHeader("Location") != null) {
// TODO: This may indicate lack of authentication or may alternatively be something we should follow
throw new IOException("Unexpected redirect during request processing. " +
"Expected response from: " + url + " but told to redirect to:" +
response.getFirstHeader("Location").getValue());
} else {
throw new IOException("Unexpected redirect during request processing. " +
"Expected response from: " + url + " but not told where to redirect to");
}
}
public String getAuthString() {
return authString;
}
/**
* Performs an HttpRequest to the supplied url using the supplied method. messageBody and headers are optional as
* not all requests will need them. There are two signatures to support calls that don't require parsing of the
* response.
*/
DataSet processRequest(String url, String method, String messageBody, Map<String, String> headers)
throws MessagingException {
return processRequest(url, method, messageBody, headers, true);
}
DataSet processRequest(String url, String method, String messageBody, Map<String, String> headers,
boolean needsParsing)
throws MessagingException {
DataSet dataset = new DataSet();
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_WEBDAV) {
Timber.v("processRequest url = '%s', method = '%s', messageBody = '%s'", url, method, messageBody);
}
if (url == null ||
method == null) {
return dataset;
}
getHttpClient();
try {
StringEntity messageEntity = null;
if (messageBody != null) {
messageEntity = new StringEntity(messageBody);
messageEntity.setContentType("text/xml");
}
InputStream istream = sendRequest(url, method, messageEntity, headers, true);
if (istream != null &&
needsParsing) {
try {
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true); //This should be a no-op on Android, but makes the tests work
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();
WebDavHandler myHandler = new WebDavHandler();
xr.setContentHandler(myHandler);
xr.parse(new InputSource(istream));
dataset = myHandler.getDataSet();
} catch (SAXException se) {
Timber.e(se, "SAXException in processRequest()");
throw new MessagingException("SAXException in processRequest() ", se);
} catch (ParserConfigurationException pce) {
Timber.e(pce, "ParserConfigurationException in processRequest()");
throw new MessagingException("ParserConfigurationException in processRequest() ", pce);
}
istream.close();
}
} catch (UnsupportedEncodingException uee) {
Timber.e(uee, "UnsupportedEncodingException: ");
throw new MessagingException("UnsupportedEncodingException in processRequest() ", uee);
} catch (IOException ioe) {
Timber.e(ioe, "IOException: ");
throw new MessagingException("IOException in processRequest() ", ioe);
}
return dataset;
}
@Override
public boolean isSendCapable() {
return true;
}
@Override
public void sendMessages(List<? extends Message> messages) throws MessagingException {
WebDavFolder tmpFolder = getFolder(mStoreConfig.getDraftsFolderName());
try {
tmpFolder.open(Folder.OPEN_MODE_RW);
List<? extends Message> retMessages = tmpFolder.appendWebDavMessages(messages);
tmpFolder.moveMessages(retMessages, getSendSpoolFolder());
} finally {
if (tmpFolder != null) {
tmpFolder.close();
}
}
}
}