summaryrefslogtreecommitdiff
path: root/mail/protocols/webdav/src/main/java
diff options
context:
space:
mode:
authortest@dangofactory.cos <you@example.com>2026-08-20 16:57:18 +0000
committertest@dangofactory.cos <you@example.com>2026-08-20 16:57:18 +0000
commit50ce5d5e6cd0d720ff437d35eace545816848428 (patch)
treeb59af206a36265cf081945e434900b32b0c43edd /mail/protocols/webdav/src/main/java
downloadmonocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz
monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2
monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip
Inital
Diffstat (limited to 'mail/protocols/webdav/src/main/java')
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ConnectionInfo.java11
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java204
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DraftsFolderProvider.java5
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java83
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ParsedMessageEnvelope.java70
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java10
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavConstants.java24
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java648
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHandler.java51
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java68
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java91
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java72
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java955
-rw-r--r--mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStoreSettings.java49
14 files changed, 2341 insertions, 0 deletions
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ConnectionInfo.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ConnectionInfo.java
new file mode 100644
index 0000000..e7eb69d
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ConnectionInfo.java
@@ -0,0 +1,11 @@
+package com.fsck.k9.mail.store.webdav;
+
+/**
+ * Simple data container for passing connection information.
+ */
+class ConnectionInfo {
+ public int statusCode;
+ public short requiredAuthType;
+ public String guessedAuthUrl;
+ public String redirectUrl;
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java
new file mode 100644
index 0000000..c1d949f
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DataSet.java
@@ -0,0 +1,204 @@
+package com.fsck.k9.mail.store.webdav;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import com.fsck.k9.logging.Timber;
+
+
+/**
+ * Maintains WebDAV data
+ */
+class DataSet {
+ private Map<String, Map<String, String>> mData = new HashMap<>();
+ private StringBuilder mUid = new StringBuilder();
+ private Map<String, String> mTempData = new HashMap<>();
+
+ public void addValue(String value, String tagName) {
+ if (tagName.equals("uid")) {
+ mUid.append(value);
+ }
+
+ if (mTempData.containsKey(tagName)) {
+ mTempData.put(tagName, mTempData.get(tagName) + value);
+ } else {
+ mTempData.put(tagName, value);
+ }
+ }
+
+ public void finish() {
+ String uid = mUid.toString();
+ if (mTempData != null) {
+ mData.put(uid, mTempData);
+ } else if (mTempData != null) {
+ //TODO: This logic is clearly wrong
+ /*
+ * Lost Data are for requests that don't include a message UID. These requests should only have a depth
+ * of one for the response so it will never get stomped over.
+ */
+ }
+
+ mUid = new StringBuilder();
+ mTempData = new HashMap<>();
+ }
+
+ /**
+ * Returns a hashmap of special folder name => special folder url
+ */
+ public Map<String, String> getSpecialFolderToUrl() {
+ // We return the first (and only) map, creating one if not present
+ return mData.isEmpty() ? Collections.<String, String>emptyMap() : mData.values().iterator().next();
+ }
+
+ /**
+ * Returns a hashmap of Message UID => Message Url
+ */
+ public Map<String, String> getUidToUrl() {
+ Map<String, String> uidToUrl = new HashMap<>();
+
+ for (String uid : mData.keySet()) {
+ Map<String, String> data = mData.get(uid);
+ String value = data.get("href");
+ if (value != null &&
+ !value.equals("")) {
+ uidToUrl.put(uid, value);
+ }
+ }
+
+ return uidToUrl;
+ }
+
+ /**
+ * Returns a hashmap of Message UID => Read Status
+ */
+ public Map<String, Boolean> getUidToRead() {
+ Map<String, Boolean> uidToRead = new HashMap<>();
+
+ for (String uid : mData.keySet()) {
+ Map<String, String> data = mData.get(uid);
+ String readStatus = data.get("read");
+ if (readStatus != null && !readStatus.equals("")) {
+ Boolean value = !readStatus.equals("0");
+ uidToRead.put(uid, value);
+ } else {
+ // We don't actually want to have null values in our hashmap,
+ // as it causes the calling code to crash with an NPE as it
+ // does a lookup in the map.
+ uidToRead.put(uid, false);
+ }
+ }
+
+ return uidToRead;
+ }
+
+ /**
+ * Returns an array of all hrefs (urls) that were received
+ */
+ public String[] getHrefs() {
+ List<String> hrefs = new ArrayList<>();
+
+ for (String uid : mData.keySet()) {
+ Map<String, String> data = mData.get(uid);
+ String href = data.get("href");
+ hrefs.add(href);
+ }
+
+ return hrefs.toArray(WebDavConstants.EMPTY_STRING_ARRAY);
+ }
+
+ /**
+ * Return an array of all Message UIDs that were received
+ */
+ public String[] getUids() {
+ List<String> uids = new ArrayList<>();
+
+ for (String uid : mData.keySet()) {
+ uids.add(uid);
+ }
+
+ return uids.toArray(WebDavConstants.EMPTY_STRING_ARRAY);
+ }
+
+ /**
+ * Returns the message count as it was retrieved
+ */
+ public int getMessageCount() {
+ // It appears that Exchange is returning responses
+ // without a visiblecount element for empty folders
+ // Which resulted in this code returning -1 (as that was
+ // the previous default.)
+ // -1 is an error condition. Now the default is empty
+ int messageCount = 0;
+
+ for (String uid : mData.keySet()) {
+ Map<String, String> data = mData.get(uid);
+ String count = data.get("visiblecount");
+
+ if (count != null &&
+ !count.equals("")) {
+ messageCount = Integer.parseInt(count);
+ }
+
+ }
+
+ return messageCount;
+ }
+
+ /**
+ * Returns a Map of message UID => ParsedMessageEnvelope
+ */
+ public Map<String, ParsedMessageEnvelope> getMessageEnvelopes() {
+ Map<String, ParsedMessageEnvelope> envelopes = new HashMap<>();
+
+ for (String uid : mData.keySet()) {
+ ParsedMessageEnvelope envelope = new ParsedMessageEnvelope();
+ Map<String, String> data = mData.get(uid);
+
+ if (data != null) {
+ for (Map.Entry<String, String> entry : data.entrySet()) {
+ String header = entry.getKey();
+ if (header.equals("read")) {
+ String read = entry.getValue();
+ boolean readStatus = !read.equals("0");
+
+ envelope.setReadStatus(readStatus);
+ } else if (header.equals("date")) {
+ /**
+ * Exchange doesn't give us rfc822 dates like it claims. The date is in the format:
+ * yyyy-MM-dd'T'HH:mm:ss.SSS<Single digit representation of timezone, so far, all instances
+ * are Z>
+ */
+ String date = entry.getValue();
+ date = date.substring(0, date.length() - 1);
+
+ DateFormat dfInput = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
+ DateFormat dfOutput = new SimpleDateFormat("EEE, d MMM yy HH:mm:ss Z", Locale.US);
+ String tempDate = "";
+
+ try {
+ Date parsedDate = dfInput.parse(date);
+ tempDate = dfOutput.format(parsedDate);
+ } catch (java.text.ParseException pe) {
+ Timber.e(pe, "Error parsing date: %s", date);
+ }
+ envelope.addHeader(header, tempDate);
+ } else {
+ envelope.addHeader(header, entry.getValue());
+ }
+ }
+ }
+
+ envelopes.put(uid, envelope);
+ }
+
+ return envelopes;
+ }
+}
+
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DraftsFolderProvider.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DraftsFolderProvider.java
new file mode 100644
index 0000000..62eb93a
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/DraftsFolderProvider.java
@@ -0,0 +1,5 @@
+package com.fsck.k9.mail.store.webdav;
+
+public interface DraftsFolderProvider {
+ String getDraftsFolder();
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java
new file mode 100644
index 0000000..09fa2f1
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/HttpGeneric.java
@@ -0,0 +1,83 @@
+package com.fsck.k9.mail.store.webdav;
+
+import com.fsck.k9.logging.Timber;
+import com.fsck.k9.mail.K9MailLib;
+
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+
+import java.net.URI;
+
+import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_WEBDAV;
+import static com.fsck.k9.mail.helper.UrlEncodingHelper.decodeUtf8;
+import static com.fsck.k9.mail.helper.UrlEncodingHelper.encodeUtf8;
+
+/**
+ * New HTTP Method that allows changing of the method and generic handling Needed for WebDAV custom methods such as
+ * SEARCH and PROPFIND
+ */
+public class HttpGeneric extends HttpEntityEnclosingRequestBase {
+ public String METHOD_NAME = "POST";
+
+ public HttpGeneric() {
+ super();
+ }
+
+ public HttpGeneric(final URI uri) {
+ super();
+ setURI(uri);
+ }
+
+ /**
+ * @throws IllegalArgumentException
+ * if the uri is invalid.
+ */
+ public HttpGeneric(final String uri) {
+ super();
+
+ Timber.v("Starting uri = '%s'", uri);
+
+ String[] urlParts = uri.split("/");
+ int length = urlParts.length;
+ String end = urlParts[length - 1];
+ String url = "";
+
+ /**
+ * We have to decode, then encode the URL because Exchange likes to not properly encode all characters
+ */
+ try {
+ if (length > 3) {
+ end = decodeUtf8(end);
+ end = encodeUtf8(end);
+ end = end.replaceAll("\\+", "%20");
+ }
+ } catch (IllegalArgumentException iae) {
+ Timber.e(iae, "IllegalArgumentException caught in HttpGeneric(String uri): %s", end);
+ }
+
+ for (int i = 0; i < length - 1; i++) {
+ if (i != 0) {
+ url = url + "/" + urlParts[i];
+ } else {
+ url = urlParts[i];
+ }
+ }
+ if (K9MailLib.isDebug() && DEBUG_PROTOCOL_WEBDAV) {
+ Timber.v("url = '%s' length = %s, end = '%s' length = %s", url, url.length(), end, end.length());
+ }
+ url = url + "/" + end;
+
+ Timber.d("url = %s", url);
+ setURI(URI.create(url));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+ public void setMethod(String method) {
+ if (method != null) {
+ METHOD_NAME = method;
+ }
+ }
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ParsedMessageEnvelope.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ParsedMessageEnvelope.java
new file mode 100644
index 0000000..efcece6
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/ParsedMessageEnvelope.java
@@ -0,0 +1,70 @@
+package com.fsck.k9.mail.store.webdav;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Holds the mappings from the name returned from Exchange to the MIME format header name
+ */
+class ParsedMessageEnvelope {
+ private static final Map<String, String> HEADER_MAPPINGS;
+ static {
+ Map<String, String> map = new HashMap<>();
+ map.put("mime-version", "MIME-Version");
+ map.put("content-type", "Content-Type");
+ map.put("subject", "Subject");
+ map.put("date", "Date");
+ map.put("thread-topic", "Thread-Topic");
+ map.put("thread-index", "Thread-Index");
+ map.put("from", "From");
+ map.put("to", "To");
+ map.put("in-reply-to", "In-Reply-To");
+ map.put("cc", "Cc");
+ map.put("getcontentlength", "Content-Length");
+ HEADER_MAPPINGS = Collections.unmodifiableMap(map);
+ }
+
+ private boolean mReadStatus = false;
+ private String mUid = "";
+ private Map<String, String> mMessageHeaders = new HashMap<>();
+ private List<String> mHeaders = new ArrayList<>();
+
+ public void addHeader(String field, String value) {
+ String headerName = HEADER_MAPPINGS.get(field);
+
+ if (headerName != null) {
+ this.mMessageHeaders.put(HEADER_MAPPINGS.get(field), value);
+ this.mHeaders.add(HEADER_MAPPINGS.get(field));
+ }
+ }
+
+ public Map<String, String> getMessageHeaders() {
+ return this.mMessageHeaders;
+ }
+
+ public String[] getHeaderList() {
+ return this.mHeaders.toArray(WebDavConstants.EMPTY_STRING_ARRAY);
+ }
+
+ public void setReadStatus(boolean status) {
+ this.mReadStatus = status;
+ }
+
+ public boolean getReadStatus() {
+ return this.mReadStatus;
+ }
+
+ public void setUid(String uid) {
+ if (uid != null) {
+ this.mUid = uid;
+ }
+ }
+
+ public String getUid() {
+ return this.mUid;
+ }
+}
+
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java
new file mode 100644
index 0000000..df32394
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/SniHostSetter.java
@@ -0,0 +1,10 @@
+package com.fsck.k9.mail.store.webdav;
+
+
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+
+public interface SniHostSetter {
+ void setSniHost(SSLSocketFactory factory, SSLSocket socket, String hostname);
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavConstants.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavConstants.java
new file mode 100644
index 0000000..43da179
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavConstants.java
@@ -0,0 +1,24 @@
+package com.fsck.k9.mail.store.webdav;
+
+/**
+ * WebDAV constants
+ */
+class WebDavConstants {
+
+ // Authentication types
+ static final short AUTH_TYPE_NONE = 0;
+ static final short AUTH_TYPE_BASIC = 1;
+ static final short AUTH_TYPE_FORM_BASED = 2;
+
+ 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
+ static final String DAV_MAIL_INBOX_FOLDER = "inbox";
+ static final String DAV_MAIL_DRAFTS_FOLDER = "drafts";
+ static final String DAV_MAIL_SPAM_FOLDER = "junkemail";
+ static final String DAV_MAIL_SEND_FOLDER = "##DavMailSubmissionURI##";
+ static final String DAV_MAIL_TRASH_FOLDER = "deleteditems";
+ static final String DAV_MAIL_OUTBOX_FOLDER = "outbox";
+ static final String DAV_MAIL_SENT_FOLDER = "sentitems";
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java
new file mode 100644
index 0000000..6fde0f2
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavFolder.java
@@ -0,0 +1,648 @@
+package com.fsck.k9.mail.store.webdav;
+
+import com.fsck.k9.logging.Timber;
+import com.fsck.k9.mail.FetchProfile;
+import com.fsck.k9.mail.Flag;
+import com.fsck.k9.mail.FolderType;
+import com.fsck.k9.mail.K9MailLib;
+import com.fsck.k9.mail.Message;
+import com.fsck.k9.mail.MessageRetrievalListener;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.entity.StringEntity;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_WEBDAV;
+import static com.fsck.k9.mail.helper.UrlEncodingHelper.encodeUtf8;
+
+/**
+ * A WebDav Folder
+ */
+public class WebDavFolder {
+ private String mName;
+ private FolderType type = FolderType.REGULAR;
+ private String mFolderUrl;
+ private boolean mIsOpen = false;
+ private int mMessageCount = 0;
+ private int mUnreadMessageCount = 0;
+ private WebDavStore store;
+
+ protected WebDavStore getStore() {
+ return store;
+ }
+
+ public WebDavFolder(WebDavStore nStore, String name) {
+ super();
+ store = nStore;
+ this.mName = name;
+ buildFolderUrl();
+ }
+
+ public FolderType getType() {
+ return type;
+ }
+
+ public void setType(FolderType type) {
+ this.type = type;
+ }
+
+ private void buildFolderUrl() {
+ String encodedName;
+ String[] urlParts = this.mName.split("/");
+ String url = "";
+ for (int i = 0, count = urlParts.length; i < count; i++) {
+ if (i != 0) {
+ url = url + "/" + encodeUtf8(urlParts[i]);
+ } else {
+ url = encodeUtf8(urlParts[i]);
+ }
+ }
+ encodedName = url;
+
+ encodedName = encodedName.replaceAll("\\+", "%20");
+
+ this.mFolderUrl = store.getUrl();
+ if (!store.getUrl().endsWith("/")) {
+ this.mFolderUrl += "/";
+ }
+ this.mFolderUrl += encodedName;
+ }
+
+ public void setUrl(String url) {
+ if (url != null) {
+ this.mFolderUrl = url;
+ }
+ }
+
+ public void open() throws MessagingException {
+ store.getHttpClient();
+ this.mIsOpen = true;
+ }
+
+ public Map<String, String> copyMessages(List<WebDavMessage> messages, WebDavFolder folder)
+ throws MessagingException {
+ moveOrCopyMessages(messages, folder.getServerId(), false);
+ return null;
+ }
+
+ public Map<String, String> moveMessages(List<WebDavMessage> messages, WebDavFolder folder)
+ throws MessagingException {
+ moveOrCopyMessages(messages, folder.getServerId(), true);
+ return null;
+ }
+
+ private void moveOrCopyMessages(List<WebDavMessage> messages, String folderName, boolean isMove)
+ throws MessagingException {
+ String[] uids = new String[messages.size()];
+
+ for (int i = 0, count = messages.size(); i < count; i++) {
+ uids[i] = messages.get(i).getUid();
+ }
+ String messageBody;
+ Map<String, String> headers = new HashMap<>();
+ Map<String, String> uidToUrl = getMessageUrls(uids);
+ String[] urls = new String[uids.length];
+
+ for (int i = 0, count = uids.length; i < count; i++) {
+ urls[i] = uidToUrl.get(uids[i]);
+ if (urls[i] == null) {
+ WebDavMessage message = messages.get(i);
+ urls[i] = message.getUrl();
+ }
+ }
+
+ messageBody = store.getMoveOrCopyMessagesReadXml(urls, isMove);
+ WebDavFolder destFolder = store.getFolder(folderName);
+ headers.put("Destination", destFolder.mFolderUrl);
+ headers.put("Brief", "t");
+ headers.put("If-Match", "*");
+ String action = (isMove ? "BMOVE" : "BCOPY");
+ Timber.v("Moving %d messages to %s", messages.size(), destFolder.mFolderUrl);
+
+ store.processRequest(mFolderUrl, action, messageBody, headers, false);
+ }
+
+ private int getMessageCount(boolean read) throws MessagingException {
+ String isRead;
+ int messageCount = 0;
+ Map<String, String> headers = new HashMap<>();
+ String messageBody;
+
+ if (read) {
+ isRead = "True";
+ } else {
+ isRead = "False";
+ }
+
+ messageBody = store.getMessageCountXml(isRead);
+ headers.put("Brief", "t");
+ DataSet dataset = store.processRequest(this.mFolderUrl, "SEARCH", messageBody, headers);
+ if (dataset != null) {
+ messageCount = dataset.getMessageCount();
+ }
+ if (K9MailLib.isDebug() && DEBUG_PROTOCOL_WEBDAV) {
+ Timber.v("Counted messages and webdav returned: %d", messageCount);
+ }
+
+ return messageCount;
+ }
+
+ public int getMessageCount() throws MessagingException {
+ open();
+ this.mMessageCount = getMessageCount(true);
+ return this.mMessageCount;
+ }
+
+ public int getUnreadMessageCount() throws MessagingException {
+ open();
+ this.mUnreadMessageCount = getMessageCount(false);
+ return this.mUnreadMessageCount;
+ }
+
+ public boolean isOpen() {
+ return this.mIsOpen;
+ }
+
+ public String getServerId() {
+ return this.mName;
+ }
+
+ public String getName() {
+ return this.mName;
+ }
+
+ public void close() {
+ this.mMessageCount = 0;
+ this.mUnreadMessageCount = 0;
+ this.mIsOpen = false;
+ }
+
+ public WebDavMessage getMessage(String uid) {
+ return new WebDavMessage(uid, this);
+ }
+
+ public List<WebDavMessage> getMessages(int start, int end, MessageRetrievalListener<WebDavMessage> listener)
+ throws MessagingException {
+ List<WebDavMessage> messages = new ArrayList<>();
+ String[] uids;
+ Map<String, String> headers = new HashMap<>();
+
+ String messageBody;
+ int prevStart = start;
+
+ /** Reverse the message range since 0 index is newest */
+ start = this.mMessageCount - end;
+ end = start + (end - prevStart);
+
+ if (start < 0 || end < 0 || end < start) {
+ throw new MessagingException(String.format(Locale.US, "Invalid message set %d %d", start, end));
+ }
+
+ if (start == 0 && end < 10) {
+ end = 10;
+ }
+
+ /** Verify authentication */
+ messageBody = store.getMessagesXml();
+
+ headers.put("Brief", "t");
+ headers.put("Range", "rows=" + start + "-" + end);
+ DataSet dataset = store.processRequest(this.mFolderUrl, "SEARCH", messageBody, headers);
+ uids = dataset.getUids();
+ Map<String, String> uidToUrl = dataset.getUidToUrl();
+
+ for (String uid : uids) {
+ WebDavMessage message = new WebDavMessage(uid, this);
+ message.setUrl(uidToUrl.get(uid));
+ messages.add(message);
+
+ if (listener != null) {
+ listener.messageFinished(message);
+ }
+ }
+
+ return messages;
+ }
+
+ public boolean areMoreMessagesAvailable(int indexOfOldestMessage, Date earliestDate) {
+ return indexOfOldestMessage > 1;
+ }
+
+ private Map<String, String> getMessageUrls(String[] uids) throws MessagingException {
+ Map<String, String> headers = new HashMap<>();
+ String messageBody;
+
+ /** Retrieve and parse the XML entity for our messages */
+ messageBody = store.getMessageUrlsXml(uids);
+ headers.put("Brief", "t");
+
+ DataSet dataset = store.processRequest(this.mFolderUrl, "SEARCH", messageBody, headers);
+
+ return dataset.getUidToUrl();
+ }
+
+ public void fetch(List<WebDavMessage> messages, FetchProfile fp, MessageRetrievalListener<WebDavMessage> listener,
+ int maxDownloadSize) throws MessagingException {
+ if (messages == null || messages.isEmpty()) {
+ return;
+ }
+
+ /**
+ * Fetch message envelope information for the array
+ */
+ if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+ fetchEnvelope(messages, listener);
+ }
+ /**
+ * Fetch message flag info for the array
+ */
+ if (fp.contains(FetchProfile.Item.FLAGS)) {
+ fetchFlags(messages, listener);
+ }
+
+ if (fp.contains(FetchProfile.Item.BODY_SANE)) {
+ if (maxDownloadSize > 0) {
+ fetchMessages(messages, listener, (maxDownloadSize / 76));
+ } else {
+ fetchMessages(messages, listener, -1);
+ }
+ }
+ if (fp.contains(FetchProfile.Item.BODY)) {
+ fetchMessages(messages, listener, -1);
+ }
+ }
+
+ /**
+ * Fetches the full messages or up to {@param lines} lines and passes them to the message parser.
+ */
+ private void fetchMessages(List<WebDavMessage> messages, MessageRetrievalListener<WebDavMessage> listener, int lines)
+ throws MessagingException {
+ WebDavHttpClient httpclient;
+ httpclient = store.getHttpClient();
+
+ /**
+ * We can't hand off to processRequest() since we need the stream to parse.
+ */
+ for (WebDavMessage wdMessage : messages) {
+ int statusCode = 0;
+
+ /**
+ * If fetch is called outside of the initial list (ie, a locally stored message), it may not have a URL
+ * associated. Verify and fix that
+ */
+ if (wdMessage.getUrl().equals("")) {
+ wdMessage.setUrl(getMessageUrls(new String[]{wdMessage.getUid()}).get(wdMessage.getUid()));
+ Timber.i("Fetching messages with UID = '%s', URL = '%s'", wdMessage.getUid(), wdMessage.getUrl());
+ if (wdMessage.getUrl().equals("")) {
+ throw new MessagingException("Unable to get URL for message");
+ }
+ }
+
+ try {
+ Timber.i("Fetching message with UID = '%s', URL = '%s'", wdMessage.getUid(), wdMessage.getUrl());
+ HttpGet httpget = new HttpGet(new URI(wdMessage.getUrl()));
+ HttpResponse response;
+ HttpEntity entity;
+
+ httpget.setHeader("translate", "f");
+ if (store.getAuthentication() == WebDavConstants.AUTH_TYPE_BASIC) {
+ httpget.setHeader("Authorization", store.getAuthString());
+ }
+ response = httpclient.executeOverride(httpget, store.getHttpContext());
+
+ statusCode = response.getStatusLine().getStatusCode();
+
+ entity = response.getEntity();
+
+ if (statusCode < 200 ||
+ statusCode > 300) {
+ throw new IOException("Error during with code " + statusCode + " during fetch: "
+ + response.getStatusLine().toString());
+ }
+
+ if (entity != null) {
+ InputStream istream = null;
+ StringBuilder buffer = new StringBuilder();
+ String tempText;
+ String resultText;
+ BufferedReader reader = null;
+ int currentLines = 0;
+
+ try {
+ istream = WebDavHttpClient.getUngzippedContent(entity);
+
+ if (lines != -1) {
+ //Convert the ungzipped input stream into a StringBuilder
+ //containing the given line count
+ reader = new BufferedReader(new InputStreamReader(istream), 8192);
+
+ while ((tempText = reader.readLine()) != null &&
+ (currentLines < lines)) {
+ buffer.append(tempText).append("\r\n");
+ currentLines++;
+ }
+
+ IOUtils.closeQuietly(istream);
+
+ resultText = buffer.toString();
+ istream = new ByteArrayInputStream(resultText.getBytes("UTF-8"));
+ }
+ //Parse either the entire message stream, or a stream of the given lines
+ wdMessage.parse(istream);
+
+ } catch (IOException ioe) {
+ Timber.e(ioe, "IOException during message parsing");
+ throw new MessagingException("I/O Error", ioe);
+ } finally {
+ IOUtils.closeQuietly(reader);
+ IOUtils.closeQuietly(istream);
+ }
+ } else {
+ Timber.v("Empty response");
+ }
+
+ } catch (IllegalArgumentException iae) {
+ Timber.e(iae, "IllegalArgumentException caught");
+ throw new MessagingException("IllegalArgumentException caught", iae);
+ } catch (URISyntaxException use) {
+ Timber.e(use, "URISyntaxException caught");
+ throw new MessagingException("URISyntaxException caught", use);
+ } catch (IOException ioe) {
+ Timber.e(ioe, "Non-success response code loading message, response code was %d, URL: %s",
+ statusCode, wdMessage.getUrl());
+ throw new MessagingException("Failure code " + statusCode, ioe);
+ }
+
+ if (listener != null) {
+ listener.messageFinished(wdMessage);
+ }
+ }
+ }
+
+ /**
+ * Fetches and sets the message flags for the supplied messages. The idea is to have this be recursive so that
+ * we do a series of medium calls instead of one large massive call or a large number of smaller calls.
+ */
+ private void fetchFlags(List<WebDavMessage> startMessages, MessageRetrievalListener<WebDavMessage> listener) throws MessagingException {
+ HashMap<String, String> headers = new HashMap<>();
+ String messageBody;
+ List<WebDavMessage> messages = new ArrayList<>(20);
+ String[] uids;
+
+ if (startMessages == null ||
+ startMessages.isEmpty()) {
+ return;
+ }
+
+ if (startMessages.size() > 20) {
+ List<WebDavMessage> newMessages = new ArrayList<>(startMessages.size() - 20);
+ for (int i = 0, count = startMessages.size(); i < count; i++) {
+ if (i < 20) {
+ messages.add(startMessages.get(i));
+ } else {
+ newMessages.add(startMessages.get(i));
+ }
+ }
+
+ fetchFlags(newMessages, listener);
+ } else {
+ messages.addAll(startMessages);
+ }
+
+ uids = new String[messages.size()];
+
+ for (int i = 0, count = messages.size(); i < count; i++) {
+ uids[i] = messages.get(i).getUid();
+ }
+
+ messageBody = store.getMessageFlagsXml(uids);
+ headers.put("Brief", "t");
+ DataSet dataset = store.processRequest(this.mFolderUrl, "SEARCH", messageBody, headers);
+
+ if (dataset == null) {
+ throw new MessagingException("Data Set from request was null");
+ }
+
+ Map<String, Boolean> uidToReadStatus = dataset.getUidToRead();
+
+ for (WebDavMessage message : messages) {
+ try {
+ message.setFlag(Flag.SEEN, uidToReadStatus.get(message.getUid()));
+ } catch (NullPointerException e) {
+ Timber.v(e, "Under some weird circumstances, " +
+ "setting the read status when syncing from webdav threw an NPE. Skipping.");
+ }
+
+ }
+ }
+
+ /**
+ * Fetches and parses the message envelopes for the supplied messages. The idea is to have this be recursive so
+ * that we do a series of medium calls instead of one large massive call or a large number of smaller calls.
+ * Call it a happy balance
+ */
+ private void fetchEnvelope(List<WebDavMessage> startMessages, MessageRetrievalListener<WebDavMessage> listener)
+ throws MessagingException {
+ Map<String, String> headers = new HashMap<>();
+ String messageBody;
+ String[] uids;
+ List<WebDavMessage> messages = new ArrayList<>(10);
+
+ if (startMessages == null ||
+ startMessages.isEmpty()) {
+ return;
+ }
+
+ if (startMessages.size() > 10) {
+ List<WebDavMessage> newMessages = new ArrayList<>(startMessages.size() - 10);
+ for (int i = 0, count = startMessages.size(); i < count; i++) {
+ if (i < 10) {
+ messages.add(i, startMessages.get(i));
+ } else {
+ newMessages.add(i - 10, startMessages.get(i));
+ }
+ }
+
+ fetchEnvelope(newMessages, listener);
+ } else {
+ messages.addAll(startMessages);
+ }
+
+ uids = new String[messages.size()];
+
+ for (int i = 0, count = messages.size(); i < count; i++) {
+ uids[i] = messages.get(i).getUid();
+ }
+
+ messageBody = store.getMessageEnvelopeXml(uids);
+ headers.put("Brief", "t");
+ DataSet dataset = store.processRequest(this.mFolderUrl, "SEARCH", messageBody, headers);
+
+ Map<String, ParsedMessageEnvelope> envelopes = dataset.getMessageEnvelopes();
+
+ for (WebDavMessage message : messages) {
+ ParsedMessageEnvelope envelope = envelopes.get(message.getUid());
+ if (envelope != null) {
+ message.setNewHeaders(envelope);
+ message.setFlag(Flag.SEEN, envelope.getReadStatus());
+ } else {
+ Timber.e("Asked to get metadata for a non-existent message: %s", message.getUid());
+ }
+
+ if (listener != null) {
+ listener.messageFinished(message);
+ }
+ }
+ }
+
+ public void setFlags(List<WebDavMessage> messages, final Set<Flag> flags, boolean value)
+ throws MessagingException {
+ String[] uids = new String[messages.size()];
+
+ for (int i = 0, count = messages.size(); i < count; i++) {
+ uids[i] = messages.get(i).getUid();
+ }
+
+ for (Flag flag : flags) {
+ if (flag == Flag.SEEN) {
+ markServerMessagesRead(uids, value);
+ } else if (flag == Flag.DELETED) {
+ deleteServerMessages(uids);
+ }
+ }
+ }
+
+ private void markServerMessagesRead(String[] uids, boolean read) throws MessagingException {
+ String messageBody;
+ Map<String, String> headers = new HashMap<>();
+ Map<String, String> uidToUrl = getMessageUrls(uids);
+ String[] urls = new String[uids.length];
+
+ for (int i = 0, count = uids.length; i < count; i++) {
+ urls[i] = uidToUrl.get(uids[i]);
+ }
+
+ messageBody = store.getMarkMessagesReadXml(urls, read);
+ headers.put("Brief", "t");
+ headers.put("If-Match", "*");
+
+ store.processRequest(this.mFolderUrl, "BPROPPATCH", messageBody, headers, false);
+ }
+
+ private void deleteServerMessages(String[] uids) throws MessagingException {
+ Map<String, String> uidToUrl = getMessageUrls(uids);
+
+ for (String uid : uids) {
+ Map<String, String> headers = new HashMap<>();
+ String url = uidToUrl.get(uid);
+ String destinationUrl = generateDeleteUrl(url);
+
+ /**
+ * If the destination is the same as the origin, assume delete forever
+ */
+ if (destinationUrl.equals(url)) {
+ headers.put("Brief", "t");
+ store.processRequest(url, "DELETE", null, headers, false);
+ } else {
+ headers.put("Destination", generateDeleteUrl(url));
+ headers.put("Brief", "t");
+ store.processRequest(url, "MOVE", null, headers, false);
+ }
+ }
+ }
+
+ private String generateDeleteUrl(String startUrl) {
+ String[] urlParts = startUrl.split("/");
+ String filename = urlParts[urlParts.length - 1];
+
+ return store.getUrl() + "Deleted%20Items/" + filename;
+ }
+
+ public Map<String, String> appendMessages(List<Message> messages) throws MessagingException {
+ appendWebDavMessages(messages);
+ return null;
+ }
+
+ public List<WebDavMessage> appendWebDavMessages(List<Message> messages) throws MessagingException {
+ List<WebDavMessage> retMessages = new ArrayList<>(messages.size());
+
+ WebDavHttpClient httpclient = store.getHttpClient();
+
+ for (Message message : messages) {
+ HttpGeneric httpmethod;
+ HttpResponse response;
+ StringEntity bodyEntity;
+ int statusCode;
+
+ try {
+ ByteArrayOutputStream out;
+
+ long size = message.getSize();
+ if (size > Integer.MAX_VALUE) {
+ throw new MessagingException("message size > Integer.MAX_VALUE!");
+ }
+ out = new ByteArrayOutputStream((int) size);
+
+ open();
+ EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream(
+ new BufferedOutputStream(out, 1024));
+ message.writeTo(msgOut);
+ msgOut.flush();
+
+ bodyEntity = new StringEntity(out.toString(), "UTF-8");
+ bodyEntity.setContentType("message/rfc822");
+
+ String messageURL = mFolderUrl;
+ if (!messageURL.endsWith("/")) {
+ messageURL += "/";
+ }
+ messageURL += encodeUtf8(message.getUid() + ":" + System.currentTimeMillis() + ".eml");
+
+ Timber.i("Uploading message as %s", messageURL);
+
+ store.sendRequest(messageURL, "PUT", bodyEntity, null, true);
+
+ WebDavMessage retMessage = new WebDavMessage(message.getUid(), this);
+
+ retMessage.setUrl(messageURL);
+ retMessages.add(retMessage);
+ } catch (Exception e) {
+ throw new MessagingException("Unable to append", e);
+ }
+ }
+ return retMessages;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof WebDavFolder) {
+ return ((WebDavFolder) o).mName.equals(mName);
+ }
+ return super.equals(o);
+ }
+
+ public String getUrl() {
+ return mFolderUrl;
+ }
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHandler.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHandler.java
new file mode 100644
index 0000000..c148dc9
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHandler.java
@@ -0,0 +1,51 @@
+package com.fsck.k9.mail.store.webdav;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.util.LinkedList;
+
+/**
+ * Handler for WebDAV XML events
+ */
+class WebDavHandler extends DefaultHandler {
+ private DataSet mDataSet = new DataSet();
+ private final LinkedList<String> mOpenTags = new LinkedList<>();
+
+ public DataSet getDataSet() {
+ return this.mDataSet;
+ }
+
+ @Override
+ public void startDocument() throws SAXException {
+ this.mDataSet = new DataSet();
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ /* Do nothing */
+ }
+
+ @Override
+ public void startElement(String namespaceURI, String localName,
+ String qName, Attributes atts) throws SAXException {
+ mOpenTags.addFirst(localName);
+ }
+
+ @Override
+ public void endElement(String namespaceURI, String localName, String qName) {
+ mOpenTags.removeFirst();
+
+ /** Reset the hash temp variables */
+ if (localName.equals("response")) {
+ this.mDataSet.finish();
+ }
+ }
+
+ @Override
+ public void characters(char ch[], int start, int length) {
+ String value = new String(ch, start, length);
+ mDataSet.addValue(value, mOpenTags.peek());
+ }
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java
new file mode 100644
index 0000000..47b424e
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavHttpClient.java
@@ -0,0 +1,68 @@
+package com.fsck.k9.mail.store.webdav;
+
+import com.fsck.k9.logging.Timber;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * HTTP client for WebDAV communication
+ */
+public class WebDavHttpClient extends DefaultHttpClient {
+ /*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
+ * the License for the specific language governing permissions and limitations under the License.
+ */
+
+ public static class WebDavHttpClientFactory {
+
+ public WebDavHttpClient create() {
+ return new WebDavHttpClient();
+ }
+ }
+
+ public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
+ Timber.i("Requesting gzipped data");
+ request.addHeader("Accept-Encoding", "gzip");
+ }
+
+ public static InputStream getUngzippedContent(HttpEntity entity)
+ throws IOException {
+ InputStream responseStream = entity.getContent();
+ if (responseStream == null)
+ return null;
+ Header header = entity.getContentEncoding();
+ if (header == null)
+ return responseStream;
+ String contentEncoding = header.getValue();
+ if (contentEncoding == null)
+ return responseStream;
+ if (contentEncoding.contains("gzip")) {
+ Timber.i("Response is gzipped");
+ responseStream = new GZIPInputStream(responseStream);
+ }
+ return responseStream;
+ }
+
+ public HttpResponse executeOverride(HttpUriRequest request, HttpContext context)
+ throws IOException {
+ modifyRequestToAcceptGzipResponse(request);
+ return super.execute(request, context);
+ }
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java
new file mode 100644
index 0000000..42f4c5b
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavMessage.java
@@ -0,0 +1,91 @@
+package com.fsck.k9.mail.store.webdav;
+
+import com.fsck.k9.logging.Timber;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.internet.MimeMessage;
+
+import java.util.Locale;
+import java.util.Map;
+
+import static com.fsck.k9.mail.helper.UrlEncodingHelper.decodeUtf8;
+import static com.fsck.k9.mail.helper.UrlEncodingHelper.encodeUtf8;
+
+/**
+ * A WebDav Message
+ */
+public class WebDavMessage extends MimeMessage {
+ private final WebDavFolder mFolder;
+ private String mUrl = "";
+
+
+ WebDavMessage(String uid, WebDavFolder folder) {
+ this.mUid = uid;
+ this.mFolder = folder;
+ }
+
+ public void setUrl(String url) {
+ // TODO: This is a not as ugly hack (ie, it will actually work). But it's still horrible
+ // XXX: prevent URLs from getting to us that are broken
+ if (!(url.toLowerCase(Locale.US).contains("http"))) {
+ if (!(url.startsWith("/"))) {
+ url = "/" + url;
+ }
+ url = ((WebDavFolder) mFolder).getUrl() + url;
+ }
+
+ String[] urlParts = url.split("/");
+ int length = urlParts.length;
+ String end = urlParts[length - 1];
+
+ this.mUrl = "";
+ url = "";
+
+ /**
+ * We have to decode, then encode the URL because Exchange likes to not properly encode all characters
+ */
+ try {
+ end = decodeUtf8(end);
+ end = encodeUtf8(end);
+ end = end.replaceAll("\\+", "%20");
+ } catch (IllegalArgumentException iae) {
+ Timber.e(iae, "IllegalArgumentException caught in setUrl: ");
+ }
+
+ for (int i = 0; i < length - 1; i++) {
+ if (i != 0) {
+ url = url + "/" + urlParts[i];
+ } else {
+ url = urlParts[i];
+ }
+ }
+
+ url = url + "/" + end;
+
+ this.mUrl = url;
+ }
+
+ public String getUrl() {
+ return this.mUrl;
+ }
+
+ public void setSize(int size) {
+ this.mSize = size;
+ }
+
+ public void setNewHeaders(ParsedMessageEnvelope envelope) throws MessagingException {
+ String[] headers = envelope.getHeaderList();
+ Map<String, String> messageHeaders = envelope.getMessageHeaders();
+ for (String header : headers) {
+ String headerValue = messageHeaders.get(header);
+ if (header.equals("Content-Length")) {
+ int size = Integer.parseInt(headerValue);
+ this.setSize(size);
+ }
+
+ if (headerValue != null &&
+ !headerValue.equals("")) {
+ this.addHeader(header, headerValue);
+ }
+ }
+ }
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java
new file mode 100644
index 0000000..36d7abd
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavSocketFactory.java
@@ -0,0 +1,72 @@
+package com.fsck.k9.mail.store.webdav;
+
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+import com.fsck.k9.mail.ssl.TrustManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import org.apache.http.conn.scheme.LayeredSocketFactory;
+import org.apache.http.params.HttpParams;
+
+
+/*
+ * TODO: find out what's going on here and document it.
+ * Using two socket factories looks suspicious.
+ */
+public class WebDavSocketFactory implements LayeredSocketFactory {
+ private final SniHostSetter sniHostSetter;
+ private SSLSocketFactory mSocketFactory;
+ private org.apache.http.conn.ssl.SSLSocketFactory mSchemeSocketFactory;
+
+ public WebDavSocketFactory(TrustManagerFactory trustManagerFactory, SniHostSetter sniHostSetter,
+ String host, int port) throws NoSuchAlgorithmException, KeyManagementException {
+ this.sniHostSetter = sniHostSetter;
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, new TrustManager[] {
+ trustManagerFactory.getTrustManagerForDomain(host, port)
+ }, null);
+ mSocketFactory = sslContext.getSocketFactory();
+ mSchemeSocketFactory = org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
+ mSchemeSocketFactory.setHostnameVerifier(
+ org.apache.http.conn.ssl.SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
+ }
+
+ public Socket connectSocket(Socket sock, String host, int port,
+ InetAddress localAddress, int localPort, HttpParams params)
+ throws IOException {
+ return mSchemeSocketFactory.connectSocket(sock, host, port, localAddress, localPort, params);
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return mSocketFactory.createSocket();
+ }
+
+ public boolean isSecure(Socket sock) throws IllegalArgumentException {
+ return mSchemeSocketFactory.isSecure(sock);
+ }
+ public Socket createSocket(
+ final Socket socket,
+ final String host,
+ final int port,
+ final boolean autoClose
+ ) throws IOException {
+ SSLSocket sslSocket = (SSLSocket) mSocketFactory.createSocket(
+ socket,
+ host,
+ port,
+ autoClose
+ );
+ sniHostSetter.setSniHost(mSocketFactory, sslSocket, host);
+ //hostnameVerifier.verify(host, sslSocket);
+ // verifyHostName() didn't blowup - good!
+ return sslSocket;
+ }
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java
new file mode 100644
index 0000000..aafe206
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStore.java
@@ -0,0 +1,955 @@
+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.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.fsck.k9.logging.Timber;
+import com.fsck.k9.mail.CertificateValidationException;
+import com.fsck.k9.mail.ConnectionSecurity;
+import com.fsck.k9.mail.FolderType;
+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.ssl.TrustManagerFactory;
+import com.fsck.k9.mail.store.webdav.WebDavHttpClient.WebDavHttpClientFactory;
+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 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 {
+ private final DraftsFolderProvider draftsFolderProvider;
+ 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 TrustManagerFactory trustManagerFactory;
+ private final SniHostSetter sniHostSetter;
+ 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 WebDavFolder sendFolder = null;
+ private Map<String, WebDavFolder> folderList = new HashMap<>();
+
+ public WebDavStore(TrustManagerFactory trustManagerFactory, SniHostSetter sniHostSetter,
+ ServerSettings serverSettings, DraftsFolderProvider draftsFolderProvider) {
+ this(trustManagerFactory, sniHostSetter, serverSettings, draftsFolderProvider,
+ new WebDavHttpClient.WebDavHttpClientFactory());
+ }
+
+ public WebDavStore(TrustManagerFactory trustManagerFactory, SniHostSetter sniHostSetter,
+ ServerSettings serverSettings, DraftsFolderProvider draftsFolderProvider,
+ WebDavHttpClientFactory clientFactory) {
+ this.draftsFolderProvider = draftsFolderProvider;
+ httpClientFactory = clientFactory;
+ this.trustManagerFactory = trustManagerFactory;
+ this.sniHostSetter = sniHostSetter;
+
+ hostname = serverSettings.host;
+ port = serverSettings.port;
+
+ mConnectionSecurity = serverSettings.connectionSecurity;
+
+ username = serverSettings.username;
+ password = serverSettings.password;
+ alias = WebDavStoreSettings.getAlias(serverSettings);
+
+ path = WebDavStoreSettings.getPath(serverSettings);
+ formBasedAuthPath = WebDavStoreSettings.getAuthPath(serverSettings);
+ mailboxPath = WebDavStoreSettings.getMailboxPath(serverSettings);
+
+
+ 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.NONE) {
+ root = "http";
+ } else {
+ root = "https";
+ }
+ root += "://" + hostname + ":" + port;
+ return root;
+ }
+
+ HttpContext getHttpContext() {
+ return httpContext;
+ }
+
+ short getAuthentication() {
+ return authenticationType;
+ }
+
+ public void checkSettings() throws MessagingException {
+ authenticate();
+ }
+
+ public List<WebDavFolder> getPersonalNamespaces() throws MessagingException {
+ List<WebDavFolder> folderList = new LinkedList<>();
+ /*
+ * We have to check authentication here so we have the proper URL stored
+ */
+ getHttpClient();
+
+ /*
+ * First we get the "special" folders list (inbox, outbox, etc)
+ */
+ 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();
+
+ /*
+ * 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, specialFoldersMap);
+ 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.
+ */
+ private WebDavFolder createFolder(String folderUrl, Map<String, String> specialFoldersMap) {
+ if (folderUrl == null) {
+ return null;
+ }
+
+ WebDavFolder wdFolder = null;
+ String folderName = getFolderName(folderUrl);
+ if (folderName != null) {
+ wdFolder = getFolder(folderName);
+ if (wdFolder != null) {
+ wdFolder.setUrl(folderUrl);
+ FolderType type = getFolderType(folderName, specialFoldersMap);
+ wdFolder.setType(type);
+ }
+ }
+ // else: Unknown URL format => NO Folder created
+
+ return wdFolder;
+ }
+
+ private FolderType getFolderType(String folderName, Map<String, String> specialFoldersMap) {
+ if (folderName.equals(getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_INBOX_FOLDER)))) {
+ return FolderType.INBOX;
+ } else if (folderName.equals(getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_DRAFTS_FOLDER)))) {
+ return FolderType.DRAFTS;
+ } else if (folderName.equals(getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_TRASH_FOLDER)))) {
+ return FolderType.TRASH;
+ } else if (folderName.equals(getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_SPAM_FOLDER)))) {
+ return FolderType.SPAM;
+ } else if (folderName.equals(getFolderName(specialFoldersMap.get(WebDavConstants.DAV_MAIL_SENT_FOLDER)))) {
+ return FolderType.SENT;
+ } else {
+ return FolderType.REGULAR;
+ }
+ }
+
+ 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;
+ }
+
+ 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 WebDavFolder getSendSpoolFolder() {
+ if (sendFolder == null) {
+ sendFolder = getFolder(WebDavConstants.DAV_MAIL_SEND_FOLDER);
+ }
+
+ return sendFolder;
+ }
+
+ 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 {
+ WebDavSocketFactory socketFactory =
+ new WebDavSocketFactory(trustManagerFactory, sniHostSetter, hostname, 443);
+ Scheme s = new Scheme("https", socketFactory, 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;
+ }
+
+ public void sendMessage(Message message) throws MessagingException {
+ List<Message> messages = Collections.singletonList(message);
+ WebDavFolder tmpFolder = getFolder(draftsFolderProvider.getDraftsFolder());
+ try {
+ tmpFolder.open();
+ List<WebDavMessage> retMessages = tmpFolder.appendWebDavMessages(messages);
+
+ tmpFolder.moveMessages(retMessages, getSendSpoolFolder());
+ } finally {
+ if (tmpFolder != null) {
+ tmpFolder.close();
+ }
+ }
+ }
+}
diff --git a/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStoreSettings.java b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStoreSettings.java
new file mode 100644
index 0000000..f418e39
--- /dev/null
+++ b/mail/protocols/webdav/src/main/java/com/fsck/k9/mail/store/webdav/WebDavStoreSettings.java
@@ -0,0 +1,49 @@
+package com.fsck.k9.mail.store.webdav;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fsck.k9.mail.ServerSettings;
+
+
+/**
+ * Extract WebDav-specific server settings from {@link ServerSettings}.
+ */
+public class WebDavStoreSettings {
+ private static final String ALIAS_KEY = "alias";
+ private static final String PATH_KEY = "path";
+ private static final String AUTH_PATH_KEY = "authPath";
+ private static final String MAILBOX_PATH_KEY = "mailboxPath";
+
+ public static String getAlias(ServerSettings serverSettings) {
+ return serverSettings.getExtra().get(ALIAS_KEY);
+ }
+
+ public static String getPath(ServerSettings serverSettings) {
+ return serverSettings.getExtra().get(PATH_KEY);
+ }
+
+ public static String getAuthPath(ServerSettings serverSettings) {
+ return serverSettings.getExtra().get(AUTH_PATH_KEY);
+ }
+
+ public static String getMailboxPath(ServerSettings serverSettings) {
+ return serverSettings.getExtra().get(MAILBOX_PATH_KEY);
+ }
+
+ public static Map<String, String> createExtra(String alias, String path, String authPath, String mailboxPath) {
+ Map<String, String> 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;
+ }
+
+ private static void putIfNotNull(Map<String, String> map, String key, String value) {
+ if (value != null) {
+ map.put(key, value);
+ }
+ }
+}