package com.fsck.k9.mail.store.webdav;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
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 timber.log.Timber;
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
*/
class WebDavFolder extends Folder<WebDavMessage> {
private String mName;
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();
}
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;
}
}
@Override
public void open(int mode) throws MessagingException {
store.getHttpClient();
this.mIsOpen = true;
}
@Override
public Map<String, String> copyMessages(List<? extends Message> messages, Folder folder) throws MessagingException {
moveOrCopyMessages(messages, folder.getName(), false);
return null;
}
@Override
public Map<String, String> moveMessages(List<? extends Message> messages, Folder folder) throws MessagingException {
moveOrCopyMessages(messages, folder.getName(), true);
return null;
}
@Override
public void delete(List<? extends Message> msgs, String trashFolderName) throws MessagingException {
moveOrCopyMessages(msgs, trashFolderName, true);
}
private void moveOrCopyMessages(List<? extends Message> 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<String, String>();
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 && messages.get(i) instanceof WebDavMessage) {
WebDavMessage wdMessage = (WebDavMessage) messages.get(i);
urls[i] = wdMessage.getUrl();
}
}
messageBody = store.getMoveOrCopyMessagesReadXml(urls, isMove);
WebDavFolder destFolder = (WebDavFolder) 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, String>();
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;
}
@Override
public int getMessageCount() throws MessagingException {
open(Folder.OPEN_MODE_RW);
this.mMessageCount = getMessageCount(true);
return this.mMessageCount;
}
@Override
public int getUnreadMessageCount() throws MessagingException {
open(Folder.OPEN_MODE_RW);
this.mUnreadMessageCount = getMessageCount(false);
return this.mUnreadMessageCount;
}
@Override
public int getFlaggedMessageCount() throws MessagingException {
return -1;
}
@Override
public boolean isOpen() {
return this.mIsOpen;
}
@Override
public int getMode() {
return Folder.OPEN_MODE_RW;
}
@Override
public String getName() {
return this.mName;
}
@Override
public boolean exists() {
return true;
}
@Override
public void close() {
this.mMessageCount = 0;
this.mUnreadMessageCount = 0;
this.mIsOpen = false;
}
@Override
public boolean create(FolderType type) throws MessagingException {
return true;
}
@Override
public void delete(boolean recursive) throws MessagingException {
throw new Error("WebDavFolder.delete() not implemeneted");
}
@Override
public WebDavMessage getMessage(String uid) throws MessagingException {
return new WebDavMessage(uid, this);
}
@Override
public List<WebDavMessage> getMessages(int start, int end, Date earliestDate, MessageRetrievalListener<WebDavMessage> listener)
throws MessagingException {
List<WebDavMessage> messages = new ArrayList<WebDavMessage>();
String[] uids;
Map<String, String> headers = new HashMap<String, String>();
int uidsLength;
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();
uidsLength = uids.length;
for (int i = 0; i < uidsLength; i++) {
if (listener != null) {
listener.messageStarted(uids[i], i, uidsLength);
}
WebDavMessage message = new WebDavMessage(uids[i], this);
message.setUrl(uidToUrl.get(uids[i]));
messages.add(message);
if (listener != null) {
listener.messageFinished(message, i, uidsLength);
}
}
return messages;
}
@Override
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, String>();
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();
}
@Override
public void fetch(List<WebDavMessage> messages, FetchProfile fp, MessageRetrievalListener<WebDavMessage> listener)
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)) {
int maximumAutoDownloadSize = store.getStoreConfig().getMaximumAutoDownloadMessageSize();
if (maximumAutoDownloadSize > 0) {
fetchMessages(messages, listener, (maximumAutoDownloadSize / 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 (int i = 0, count = messages.size(); i < count; i++) {
WebDavMessage wdMessage = messages.get(i);
int statusCode = 0;
if (listener != null) {
listener.messageStarted(wdMessage.getUid(), i, count);
}
/**
* 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, i, count);
}
}
}
/**
* 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, String>();
String messageBody;
List<Message> messages = new ArrayList<Message>(20);
String[] uids;
if (startMessages == null ||
startMessages.isEmpty()) {
return;
}
if (startMessages.size() > 20) {
List<WebDavMessage> newMessages = new ArrayList<WebDavMessage>(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 (int i = 0, count = messages.size(); i < count; i++) {
if (!(messages.get(i) instanceof WebDavMessage)) {
throw new MessagingException("WebDavStore fetch called with non-WebDavMessage");
}
WebDavMessage wdMessage = (WebDavMessage) messages.get(i);
try {
wdMessage.setFlagInternal(Flag.SEEN, uidToReadStatus.get(wdMessage.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, String>();
String messageBody;
String[] uids;
List<WebDavMessage> messages = new ArrayList<WebDavMessage>(10);
if (startMessages == null ||
startMessages.isEmpty()) {
return;
}
if (startMessages.size() > 10) {
List<WebDavMessage> newMessages = new ArrayList<WebDavMessage>(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();
int count = messages.size();
for (int i = messages.size() - 1; i >= 0; i--) {
WebDavMessage message = messages.get(i);
if (listener != null) {
listener.messageStarted(messages.get(i).getUid(), i, count);
}
ParsedMessageEnvelope envelope = envelopes.get(message.getUid());
if (envelope != null) {
message.setNewHeaders(envelope);
message.setFlagInternal(Flag.SEEN, envelope.getReadStatus());
} else {
Timber.e("Asked to get metadata for a non-existent message: %s", message.getUid());
}
if (listener != null) {
listener.messageFinished(messages.get(i), i, count);
}
}
}
@Override
public void setFlags(List<? extends Message> 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<String, String>();
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, String>();
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;
}
@Override
public Map<String, String> appendMessages(List<? extends Message> messages) throws MessagingException {
appendWebDavMessages(messages);
return null;
}
public List<? extends Message> appendWebDavMessages(List<? extends Message> messages) throws MessagingException {
List<Message> retMessages = new ArrayList<Message>(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(Folder.OPEN_MODE_RW);
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);
}
@Override
public String getUidFromMessageId(Message message) throws MessagingException {
Timber.e("Unimplemented method getUidFromMessageId in WebDavStore.WebDavFolder could lead to duplicate messages "
+ " being uploaded to the Sent folder");
return null;
}
@Override
public void setFlags(final Set<Flag> flags, boolean value) throws MessagingException {
Timber.e("Unimplemented method setFlags(Set<Flag>, boolean) breaks markAllMessagesAsRead and EmptyTrash");
// Try to make this efficient by not retrieving all of the messages
}
public String getUrl() {
return mFolderUrl;
}
}