diff options
| author | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
|---|---|---|
| committer | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
| commit | 50ce5d5e6cd0d720ff437d35eace545816848428 (patch) | |
| tree | b59af206a36265cf081945e434900b32b0c43edd /mail/protocols/webdav/src/main/java | |
| download | monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2 monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip | |
Inital
Diffstat (limited to 'mail/protocols/webdav/src/main/java')
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); + } + } +} |
