2057 lines
83 KiB
Java
2057 lines
83 KiB
Java
/* Copyright (C) 2012 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.
|
|
*/
|
|
|
|
package com.android.imap2;
|
|
|
|
import android.content.ContentProviderOperation;
|
|
import android.content.ContentProviderOperation.Builder;
|
|
import android.content.ContentResolver;
|
|
import android.content.ContentUris;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.content.OperationApplicationException;
|
|
import android.database.Cursor;
|
|
import android.net.TrafficStats;
|
|
import android.os.Bundle;
|
|
import android.os.RemoteException;
|
|
import android.util.Log;
|
|
|
|
import com.android.emailcommon.TrafficFlags;
|
|
import com.android.emailcommon.internet.MimeUtility;
|
|
import com.android.emailcommon.mail.Address;
|
|
import com.android.emailcommon.mail.CertificateValidationException;
|
|
import com.android.emailcommon.mail.MessagingException;
|
|
import com.android.emailcommon.provider.Account;
|
|
import com.android.emailcommon.provider.EmailContent.Attachment;
|
|
import com.android.emailcommon.provider.EmailContent.Body;
|
|
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
|
|
import com.android.emailcommon.provider.EmailContent.MessageColumns;
|
|
import com.android.emailcommon.provider.EmailContent.SyncColumns;
|
|
import com.android.emailcommon.provider.EmailContent;
|
|
import com.android.emailcommon.provider.HostAuth;
|
|
import com.android.emailcommon.provider.Mailbox;
|
|
import com.android.emailcommon.provider.MailboxUtilities;
|
|
import com.android.emailcommon.provider.ProviderUnavailableException;
|
|
import com.android.emailcommon.provider.EmailContent.Message;
|
|
import com.android.emailcommon.service.EmailServiceProxy;
|
|
import com.android.emailcommon.service.EmailServiceStatus;
|
|
import com.android.emailcommon.service.SyncWindow;
|
|
import com.android.emailcommon.utility.SSLUtils;
|
|
import com.android.emailcommon.utility.TextUtilities;
|
|
import com.android.emailcommon.utility.Utility;
|
|
import com.android.emailsync.AbstractSyncService;
|
|
import com.android.emailsync.PartRequest;
|
|
import com.android.emailsync.Request;
|
|
import com.android.emailsync.SyncManager;
|
|
import com.android.mail.providers.UIProvider;
|
|
import com.beetstra.jutf7.CharsetProvider;
|
|
|
|
import java.io.BufferedWriter;
|
|
import java.io.IOException;
|
|
import java.io.OutputStreamWriter;
|
|
import java.io.Writer;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.Socket;
|
|
import java.net.SocketAddress;
|
|
import java.net.SocketException;
|
|
import java.net.SocketTimeoutException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.Charset;
|
|
import java.text.ParseException;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Date;
|
|
import java.util.Stack;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import javax.net.ssl.HttpsURLConnection;
|
|
import javax.net.ssl.SSLException;
|
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
|
import javax.net.ssl.SSLSession;
|
|
import javax.net.ssl.SSLSocket;
|
|
|
|
public class Imap2SyncService extends AbstractSyncService {
|
|
|
|
private static final String IMAP_OK = "OK";
|
|
private static final SimpleDateFormat GMAIL_INTERNALDATE_FORMAT =
|
|
new SimpleDateFormat("EEE, dd MMM yy HH:mm:ss z");
|
|
private static final String IMAP_ERR = "ERR";
|
|
|
|
private static final SimpleDateFormat IMAP_DATE_FORMAT =
|
|
new SimpleDateFormat("dd-MMM-yyyy");
|
|
private static final SimpleDateFormat INTERNALDATE_FORMAT =
|
|
new SimpleDateFormat("dd-MMM-yy HH:mm:ss z");
|
|
private static final Charset MODIFIED_UTF_7_CHARSET =
|
|
new CharsetProvider().charsetForName("X-RFC-3501");
|
|
|
|
public static final String IMAP_DELETED_MESSAGES_FOLDER_NAME = "AndroidMail Trash";
|
|
public static final String GMAIL_TRASH_FOLDER = "[Gmail]/Trash";
|
|
|
|
private static Pattern IMAP_RESPONSE_PATTERN = Pattern.compile("\\*(\\s(\\d+))?\\s(\\w+).*");
|
|
|
|
private static final int HEADER_BATCH_COUNT = 10;
|
|
|
|
// private static final int IDLE_TIMEOUT_MILLIS = 12*MINS;
|
|
private static final int SECONDS = 1000;
|
|
private static final int MINS = 60*SECONDS;
|
|
private static final int IDLE_ASLEEP_MILLIS = 11*MINS;
|
|
// private static final int COMMAND_TIMEOUT_MILLIS = 24*SECS;
|
|
|
|
private static final int SOCKET_CONNECT_TIMEOUT = 10*SECONDS;
|
|
private static final int SOCKET_TIMEOUT = 20*SECONDS;
|
|
|
|
private ContentResolver mResolver;
|
|
private int mWriterTag = 1;
|
|
private boolean mIsGmail = false;
|
|
private boolean mIsIdle = false;
|
|
private int mLastExists = -1;
|
|
|
|
private ArrayList<String> mImapResponse = null;
|
|
private String mImapResult;
|
|
private String mImapErrorLine = null;
|
|
|
|
private Socket mSocket = null;
|
|
private boolean mStop = false;
|
|
|
|
public int mServiceResult = 0;
|
|
private boolean mIsServiceRequestPending = false;
|
|
|
|
private final String[] MAILBOX_SERVER_ID_ARGS = new String[2];
|
|
public Imap2SyncService() {
|
|
this("Imap2 Validation");
|
|
}
|
|
|
|
private final ArrayList<Integer> SERVER_DELETES = new ArrayList<Integer>();
|
|
|
|
private static final String INBOX_SERVER_NAME = "Inbox"; // Per RFC3501
|
|
|
|
private BufferedWriter mWriter;
|
|
private ImapInputStream mReader;
|
|
|
|
private HostAuth mHostAuth;
|
|
private String mPrefix;
|
|
|
|
public Imap2SyncService(Context _context, Mailbox _mailbox) {
|
|
super(_context, _mailbox);
|
|
mResolver = _context.getContentResolver();
|
|
MAILBOX_SERVER_ID_ARGS[0] = Long.toString(mMailboxId);
|
|
}
|
|
|
|
private Imap2SyncService(String prefix) {
|
|
super(prefix);
|
|
}
|
|
|
|
public Imap2SyncService(Context _context, Account _account) {
|
|
this("Imap2 Account");
|
|
mContext = _context;
|
|
mResolver = _context.getContentResolver();
|
|
mAccount = _account;
|
|
mHostAuth = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
|
|
mPrefix = mHostAuth.mDomain;
|
|
}
|
|
|
|
@Override
|
|
public boolean alarm() {
|
|
// See if we've got anything to do...
|
|
Cursor updates = getUpdatesCursor();
|
|
Cursor deletes = getDeletesCursor();
|
|
try {
|
|
if (mRequestQueue.isEmpty() && updates == null && deletes == null) {
|
|
userLog("Ping: nothing to do");
|
|
} else {
|
|
int cnt = mRequestQueue.size();
|
|
if (updates != null) {
|
|
cnt += updates.getCount();
|
|
}
|
|
if (deletes != null) {
|
|
cnt += deletes.getCount();
|
|
}
|
|
userLog("Ping: " + cnt + " tasks");
|
|
ping();
|
|
}
|
|
} finally {
|
|
if (updates != null) {
|
|
updates.close();
|
|
}
|
|
if (deletes != null) {
|
|
deletes.close();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void reset() {
|
|
// TODO Auto-generated method stub
|
|
}
|
|
|
|
public void addRequest(Request req) {
|
|
super.addRequest(req);
|
|
if (req instanceof PartRequest) {
|
|
userLog("Request for attachment " + ((PartRequest)req).mAttachment.mId);
|
|
}
|
|
ping();
|
|
}
|
|
|
|
@Override
|
|
public Bundle validateAccount(HostAuth hostAuth, Context context) {
|
|
Bundle bundle = new Bundle();
|
|
int resultCode = MessagingException.IOERROR;
|
|
|
|
Connection conn = connectAndLogin(hostAuth, "main");
|
|
if (conn.status == EXIT_DONE) {
|
|
resultCode = MessagingException.NO_ERROR;
|
|
} else if (conn.status == EXIT_LOGIN_FAILURE) {
|
|
resultCode = MessagingException.AUTHENTICATION_FAILED;
|
|
}
|
|
|
|
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
|
|
return bundle;
|
|
}
|
|
|
|
public void loadFolderList() throws IOException {
|
|
HostAuth hostAuth =
|
|
HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
|
|
if (hostAuth == null) return;
|
|
Connection conn = connectAndLogin(hostAuth, "folderList");
|
|
if (conn.status == EXIT_DONE) {
|
|
setConnection(conn);
|
|
readFolderList();
|
|
conn.socket.close();
|
|
}
|
|
}
|
|
|
|
private void setConnection(Connection conn) {
|
|
mConnection = conn;
|
|
mWriter = conn.writer;
|
|
mReader = conn.reader;
|
|
mSocket = conn.socket;
|
|
}
|
|
|
|
@Override
|
|
public void resetCalendarSyncKey() {
|
|
// Not used by Imap2
|
|
}
|
|
|
|
public void ping() {
|
|
mIsServiceRequestPending = true;
|
|
Imap2SyncManager.runAwake(mMailbox.mId);
|
|
if (mSocket != null) {
|
|
try {
|
|
if (mIsIdle) {
|
|
userLog("breakIdle; sending DONE...");
|
|
mWriter.write("DONE\r\n");
|
|
mWriter.flush();
|
|
}
|
|
} catch (SocketException e) {
|
|
} catch (IOException e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
public void stop () {
|
|
if (mSocket != null)
|
|
try {
|
|
if (mIsIdle)
|
|
ping();
|
|
mSocket.close();
|
|
} catch (IOException e) {
|
|
}
|
|
mStop = true;
|
|
}
|
|
|
|
public String writeCommand (Writer out, String cmd) {
|
|
try {
|
|
Integer t = mWriterTag++;
|
|
String tag = "@@a" + t + ' ';
|
|
out.write(tag);
|
|
out.write(cmd);
|
|
out.write("\r\n");
|
|
out.flush();
|
|
if (!cmd.startsWith("login"))
|
|
userLog(tag + cmd);
|
|
return tag;
|
|
} catch (IOException e) {
|
|
userLog("IOException in writeCommand");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private long readLong (String str, int idx) {
|
|
char ch = str.charAt(idx);
|
|
long num = 0;
|
|
while (ch >= '0' && ch <= '9') {
|
|
num = (num * 10) + (ch - '0');
|
|
ch = str.charAt(++idx);
|
|
}
|
|
return num;
|
|
}
|
|
|
|
private void readUntagged(String str) {
|
|
// Skip the "* "
|
|
Parser p = new Parser(str, 2);
|
|
String type = p.parseAtom();
|
|
int val = -1;
|
|
if (type != null) {
|
|
char c = type.charAt(0);
|
|
if (c >= '0' && c <= '9')
|
|
try {
|
|
val = Integer.parseInt(type);
|
|
type = p.parseAtom();
|
|
if (p != null) {
|
|
if (type.toLowerCase().equals("exists"))
|
|
mLastExists = val;
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
} else if (mMailbox.mSyncKey == null || mMailbox.mSyncKey == "0") {
|
|
str = str.toLowerCase();
|
|
int idx = str.indexOf("uidvalidity");
|
|
if (idx > 0) {
|
|
//*** 12?
|
|
long num = readLong(str, idx + 12);
|
|
mMailbox.mSyncKey = Long.toString(num);
|
|
ContentValues cv = new ContentValues();
|
|
cv.put(MailboxColumns.SYNC_KEY, mMailbox.mSyncKey);
|
|
mContext.getContentResolver().update(
|
|
ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId), cv,
|
|
null, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
userLog("Untagged: " + type);
|
|
}
|
|
|
|
private boolean caseInsensitiveStartsWith(String str, String tag) {
|
|
return str.toLowerCase().startsWith(tag.toLowerCase());
|
|
}
|
|
|
|
private String readResponse (ImapInputStream r, String tag) throws IOException {
|
|
return readResponse(r, tag, null);
|
|
}
|
|
|
|
private String readResponse (ImapInputStream r, String tag, String command) throws IOException {
|
|
mImapResult = IMAP_ERR;
|
|
String str = null;
|
|
if (command != null)
|
|
mImapResponse = new ArrayList<String>();
|
|
while (true) {
|
|
str = r.readLine();
|
|
userLog("< " + str);
|
|
if (caseInsensitiveStartsWith(str, tag)) {
|
|
// This is the response from the command named 'tag'
|
|
Parser p = new Parser(str, tag.length() - 1);
|
|
mImapResult = p.parseAtom();
|
|
break;
|
|
} else if (str.charAt(0) == '*') {
|
|
if (command != null) {
|
|
Matcher m = IMAP_RESPONSE_PATTERN.matcher(str);
|
|
if (m.matches() && m.group(3).equals(command)) {
|
|
mImapResponse.add(str);
|
|
} else
|
|
readUntagged(str);
|
|
} else
|
|
readUntagged(str);
|
|
} else if (!mImapResponse.isEmpty()) {
|
|
// Continuation with string literal, perhaps?
|
|
int off = mImapResponse.size() - 1;
|
|
mImapResponse.set(off, mImapResponse.get(off) + "\r\n" + str);
|
|
}
|
|
}
|
|
|
|
if (!mImapResult.equals(IMAP_OK)) {
|
|
userLog("$$$ Error result = " + mImapResult);
|
|
mImapErrorLine = str;
|
|
}
|
|
return mImapResult;
|
|
}
|
|
|
|
String parseRecipientList (String str) {
|
|
if (str == null)
|
|
return null;
|
|
ArrayList<Address> list = new ArrayList<Address>();
|
|
String r;
|
|
Parser p = new Parser(str);
|
|
while ((r = p.parseList()) != null) {
|
|
Parser rp = new Parser(r);
|
|
String displayName = rp.parseString();
|
|
rp.parseString();
|
|
String emailAddress = rp.parseString() + "@" + rp.parseString();
|
|
list.add(new Address(emailAddress, displayName));
|
|
}
|
|
return Address.pack(list.toArray(new Address[list.size()]));
|
|
}
|
|
|
|
String parseRecipients (Parser p, Message msg) {
|
|
msg.mFrom = parseRecipientList(p.parseListOrNil());
|
|
@SuppressWarnings("unused")
|
|
String senderList = parseRecipientList(p.parseListOrNil());
|
|
msg.mReplyTo = parseRecipientList(p.parseListOrNil());
|
|
msg.mTo = parseRecipientList(p.parseListOrNil());
|
|
msg.mCc = parseRecipientList(p.parseListOrNil());
|
|
msg.mBcc = parseRecipientList(p.parseListOrNil());
|
|
return Address.toFriendly(Address.unpack(msg.mFrom));
|
|
}
|
|
|
|
private Message createMessage (String str) {
|
|
Parser p = new Parser(str, str.indexOf('(') + 1);
|
|
Date date = null;
|
|
String subject = null;
|
|
String sender = null;
|
|
boolean read = false;
|
|
int flag = 0;
|
|
String flags = null;
|
|
int uid = 0;
|
|
boolean bodystructure = false;
|
|
|
|
Message msg = new Message();
|
|
msg.mMailboxKey = mMailboxId;
|
|
|
|
try {
|
|
while (true) {
|
|
String atm = p.parseAtom();
|
|
// We're done if we have all of these, regardless of order
|
|
if (date != null && flags != null && bodystructure)
|
|
break;
|
|
// Not sure if this case is possible
|
|
if (atm == null)
|
|
break;
|
|
if (atm.equalsIgnoreCase("UID")) {
|
|
uid = p.parseInteger();
|
|
//userLog("UID=" + uid);
|
|
} else if (atm.equalsIgnoreCase("ENVELOPE")) {
|
|
String envelope = p.parseList();
|
|
Parser ep = new Parser(envelope);
|
|
ep.skipWhite();
|
|
//date = parseDate(ep.parseString());
|
|
ep.parseString();
|
|
subject = ep.parseString();
|
|
sender = parseRecipients(ep, msg);
|
|
} else if (atm.equalsIgnoreCase("FLAGS")) {
|
|
flags = p.parseList().toLowerCase();
|
|
if (flags.indexOf("\\seen") >=0)
|
|
read = true;
|
|
if (flags.indexOf("\\flagged") >=0)
|
|
flag = 1;
|
|
} else if (atm.equalsIgnoreCase("BODYSTRUCTURE")) {
|
|
msg.mSyncData = p.parseList();
|
|
bodystructure = true;
|
|
//parseBodystructure(msg, new Parser(bs), "", 1, parts);
|
|
} else if (atm.equalsIgnoreCase("INTERNALDATE")) {
|
|
date = parseInternaldate(p.parseString());
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
// Parsing error here. We've got one known one from EON
|
|
// in which BODYSTRUCTURE is ( "MIXED" (....) )
|
|
if (sender == null)
|
|
sender = "Unknown sender";
|
|
if (subject == null)
|
|
subject = "No subject";
|
|
e.printStackTrace();
|
|
}
|
|
|
|
if (subject != null && subject.startsWith("=?"))
|
|
subject = MimeUtility.decode(subject);
|
|
msg.mSubject = subject;
|
|
|
|
//msg.bodyId = 0;
|
|
//msg.parts = parts.toString();
|
|
msg.mAccountKey = mAccount.mId;
|
|
|
|
msg.mFlagLoaded = Message.FLAG_LOADED_UNLOADED;
|
|
msg.mFlags = flag;
|
|
if (read)
|
|
msg.mFlagRead = true;
|
|
msg.mTimeStamp = ((date != null) ? date : new Date()).getTime();
|
|
msg.mServerId = Long.toString(uid);
|
|
return msg;
|
|
}
|
|
|
|
private Date parseInternaldate (String str) {
|
|
if (str != null) {
|
|
SimpleDateFormat f = INTERNALDATE_FORMAT;
|
|
if (str.charAt(3) == ',')
|
|
f = GMAIL_INTERNALDATE_FORMAT;
|
|
try {
|
|
return f.parse(str);
|
|
} catch (ParseException e) {
|
|
userLog("Unparseable date: " + str);
|
|
}
|
|
}
|
|
return new Date();
|
|
}
|
|
|
|
private long getIdForUid(int uid) {
|
|
// TODO: Rename this
|
|
MAILBOX_SERVER_ID_ARGS[1] = Integer.toString(uid);
|
|
Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
|
|
MessageColumns.MAILBOX_KEY + "=? AND " + SyncColumns.SERVER_ID + "=?",
|
|
MAILBOX_SERVER_ID_ARGS, null);
|
|
try {
|
|
if (c != null && c.moveToFirst()) {
|
|
return c.getLong(Message.ID_COLUMNS_ID_COLUMN);
|
|
}
|
|
} finally {
|
|
if (c != null) {
|
|
c.close();
|
|
}
|
|
}
|
|
return Message.NO_MESSAGE;
|
|
}
|
|
|
|
private void processDelete(int uid) {
|
|
SERVER_DELETES.clear();
|
|
SERVER_DELETES.add(uid);
|
|
processServerDeletes(SERVER_DELETES);
|
|
}
|
|
|
|
/**
|
|
* Handle a single untagged line
|
|
* TODO: Perhaps batch operations for multiple lines into a single transaction
|
|
*/
|
|
private boolean handleUntagged (String line) {
|
|
line = line.toLowerCase();
|
|
Matcher m = IMAP_RESPONSE_PATTERN.matcher(line);
|
|
boolean res = false;
|
|
if (m.matches()) {
|
|
// What kind of thing is this?
|
|
String type = m.group(3);
|
|
if (type.equals("fetch") || type.equals("expunge")) {
|
|
// This is a flag change or an expunge. First, find the UID
|
|
int uid = 0;
|
|
// TODO Get rid of hack to avoid uid...
|
|
int uidPos = line.indexOf("uid");
|
|
if (uidPos > 0) {
|
|
Parser p = new Parser(line, uidPos + 3);
|
|
uid = p.parseInteger();
|
|
}
|
|
|
|
if (uid == 0) {
|
|
// This will be very inefficient
|
|
// Have to 1) break idle, 2) query the server for uid
|
|
return false;
|
|
}
|
|
long id = getIdForUid(uid);
|
|
if (id == Message.NO_MESSAGE) {
|
|
// Nothing to do; log
|
|
userLog("? No message found for uid " + uid);
|
|
return true;
|
|
}
|
|
|
|
if (type.equals("fetch")) {
|
|
if (line.indexOf("\\deleted") > 0) {
|
|
processDelete(uid);
|
|
} else {
|
|
boolean read = line.indexOf("\\seen") > 0;
|
|
boolean flagged = line.indexOf("\\flagged") > 0;
|
|
// TODO: Reuse
|
|
ContentValues values = new ContentValues();
|
|
values.put(MessageColumns.FLAG_READ, read);
|
|
values.put(MessageColumns.FLAG_FAVORITE, flagged);
|
|
mResolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id),
|
|
values, null, null);
|
|
}
|
|
userLog("<<< FLAGS " + uid);
|
|
} else {
|
|
userLog("<<< EXPUNGE " + uid);
|
|
processDelete(uid);
|
|
}
|
|
} else if (type.equals("exists")) {
|
|
int num = Integer.parseInt(m.group(2));
|
|
if (mIsGmail && (num == mLastExists)) {
|
|
userLog("Gmail: nothing new...");
|
|
return false;
|
|
}
|
|
else if (mIsGmail)
|
|
mLastExists = num;
|
|
res = true;
|
|
userLog("<<< EXISTS tag; new SEARCH");
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Prepends the folder name with the given prefix and UTF-7 encodes it.
|
|
*/
|
|
private String encodeFolderName(String name) {
|
|
// do NOT add the prefix to the special name "INBOX"
|
|
if ("inbox".equalsIgnoreCase(name)) return name;
|
|
|
|
// Prepend prefix
|
|
if (mPrefix != null) {
|
|
name = mPrefix + name;
|
|
}
|
|
|
|
// TODO bypass the conversion if name doesn't have special char.
|
|
ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
|
|
byte[] b = new byte[bb.limit()];
|
|
bb.get(b);
|
|
|
|
return Utility.fromAscii(b);
|
|
}
|
|
|
|
/**
|
|
* UTF-7 decodes the folder name and removes the given path prefix.
|
|
*/
|
|
static String decodeFolderName(String name, String prefix) {
|
|
// TODO bypass the conversion if name doesn't have special char.
|
|
String folder;
|
|
folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
|
|
if ((prefix != null) && folder.startsWith(prefix)) {
|
|
folder = folder.substring(prefix.length());
|
|
}
|
|
return folder;
|
|
}
|
|
|
|
private static class ServerUpdate {
|
|
final long id;
|
|
final int serverId;
|
|
|
|
ServerUpdate(long _id, int _serverId) {
|
|
id = _id;
|
|
serverId = _serverId;
|
|
}
|
|
}
|
|
|
|
private ArrayList<Long> mUpdatedIds = new ArrayList<Long>();
|
|
private ArrayList<Long> mDeletedIds = new ArrayList<Long>();
|
|
private Stack<ServerUpdate> mDeletes = new Stack<ServerUpdate>();
|
|
private Stack<ServerUpdate> mReadUpdates = new Stack<ServerUpdate>();
|
|
private Stack<ServerUpdate> mUnreadUpdates = new Stack<ServerUpdate>();
|
|
private Stack<ServerUpdate> mFlaggedUpdates = new Stack<ServerUpdate>();
|
|
private Stack<ServerUpdate> mUnflaggedUpdates = new Stack<ServerUpdate>();
|
|
|
|
private Cursor getUpdatesCursor() {
|
|
Cursor c = mResolver.query(Message.UPDATED_CONTENT_URI, UPDATE_DELETE_PROJECTION,
|
|
MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
|
|
if (c == null || c.getCount() == 0) {
|
|
c.close();
|
|
return null;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
private static final String[] UPDATE_DELETE_PROJECTION =
|
|
new String[] {MessageColumns.ID, SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
|
|
MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE};
|
|
private static final int UPDATE_DELETE_ID_COLUMN = 0;
|
|
private static final int UPDATE_DELETE_SERVER_ID_COLUMN = 1;
|
|
private static final int UPDATE_DELETE_MAILBOX_KEY_COLUMN = 2;
|
|
private static final int UPDATE_DELETE_READ_COLUMN = 3;
|
|
private static final int UPDATE_DELETE_FAVORITE_COLUMN = 4;
|
|
|
|
private Cursor getDeletesCursor() {
|
|
Cursor c = mResolver.query(Message.DELETED_CONTENT_URI, UPDATE_DELETE_PROJECTION,
|
|
MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
|
|
if (c == null || c.getCount() == 0) {
|
|
c.close();
|
|
return null;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
private void handleLocalDeletes() throws IOException {
|
|
Cursor c = getDeletesCursor();
|
|
if (c == null) return;
|
|
mDeletes.clear();
|
|
mDeletedIds.clear();
|
|
|
|
try {
|
|
while (c.moveToNext()) {
|
|
long id = c.getLong(UPDATE_DELETE_ID_COLUMN);
|
|
mDeletes.add(new ServerUpdate(id, c.getInt(UPDATE_DELETE_SERVER_ID_COLUMN)));
|
|
mDeletedIds.add(id);
|
|
}
|
|
sendUpdate(mDeletes, "+FLAGS (\\Deleted)");
|
|
String tag = writeCommand(mConnection.writer, "expunge");
|
|
readResponse(mConnection.reader, tag, "expunge");
|
|
|
|
// Delete the deletions now (we must go deeper!)
|
|
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
|
|
for (long id: mDeletedIds) {
|
|
ops.add(ContentProviderOperation.newDelete(
|
|
ContentUris.withAppendedId(
|
|
Message.DELETED_CONTENT_URI, id)).build());
|
|
}
|
|
applyBatch(ops);
|
|
} finally {
|
|
c.close();
|
|
}
|
|
}
|
|
|
|
private void handleLocalUpdates() throws IOException {
|
|
Cursor updatesCursor = getUpdatesCursor();
|
|
if (updatesCursor == null) return;
|
|
|
|
mUpdatedIds.clear();
|
|
mReadUpdates.clear();
|
|
mUnreadUpdates.clear();
|
|
mFlaggedUpdates.clear();
|
|
mUnflaggedUpdates.clear();
|
|
|
|
try {
|
|
while (updatesCursor.moveToNext()) {
|
|
long id = updatesCursor.getLong(UPDATE_DELETE_ID_COLUMN);
|
|
// Keep going if there's no serverId
|
|
int serverId = updatesCursor.getInt(UPDATE_DELETE_SERVER_ID_COLUMN);
|
|
if (serverId == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Say we've handled this update
|
|
mUpdatedIds.add(id);
|
|
// We have the id of the changed item. But first, we have to find out its current
|
|
// state, since the updated table saves the opriginal state
|
|
Cursor currentCursor = mResolver.query(
|
|
ContentUris.withAppendedId(Message.CONTENT_URI, id),
|
|
UPDATE_DELETE_PROJECTION, null, null, null);
|
|
try {
|
|
// If this item no longer exists (shouldn't be possible), just move along
|
|
if (!currentCursor.moveToFirst()) {
|
|
continue;
|
|
}
|
|
|
|
boolean flagChange = false;
|
|
boolean readChange = false;
|
|
|
|
long mailboxId = currentCursor.getLong(UPDATE_DELETE_MAILBOX_KEY_COLUMN);
|
|
// If the message is now in the trash folder, it has been deleted by the user
|
|
if (mailboxId != updatesCursor.getLong(UPDATE_DELETE_MAILBOX_KEY_COLUMN)) {
|
|
// The message has been moved to another mailbox
|
|
Mailbox newMailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
|
|
if (newMailbox == null) {
|
|
continue;
|
|
}
|
|
copyMessage(serverId, newMailbox);
|
|
}
|
|
|
|
// We can only send flag changes to the server in 12.0 or later
|
|
int flag = currentCursor.getInt(UPDATE_DELETE_FAVORITE_COLUMN);
|
|
if (flag != updatesCursor.getInt(UPDATE_DELETE_FAVORITE_COLUMN)) {
|
|
flagChange = true;
|
|
}
|
|
|
|
int read = currentCursor.getInt(UPDATE_DELETE_READ_COLUMN);
|
|
if (read != updatesCursor.getInt(UPDATE_DELETE_READ_COLUMN)) {
|
|
readChange = true;
|
|
}
|
|
|
|
if (!flagChange && !readChange) {
|
|
// In this case, we've got nothing to send to the server
|
|
continue;
|
|
}
|
|
|
|
ServerUpdate update = new ServerUpdate(id, serverId);
|
|
if (readChange) {
|
|
if (read == 1) {
|
|
mReadUpdates.add(update);
|
|
} else {
|
|
mUnreadUpdates.add(update);
|
|
}
|
|
}
|
|
if (flagChange) {
|
|
if (flag == 1) {
|
|
mFlaggedUpdates.add(update);
|
|
} else {
|
|
mUnflaggedUpdates.add(update);
|
|
}
|
|
}
|
|
} finally {
|
|
currentCursor.close();
|
|
}
|
|
}
|
|
} finally {
|
|
updatesCursor.close();
|
|
}
|
|
|
|
if (!mUpdatedIds.isEmpty()) {
|
|
sendUpdate(mReadUpdates, "+FLAGS (\\Seen)");
|
|
sendUpdate(mUnreadUpdates, "-FLAGS (\\Seen)");
|
|
sendUpdate(mFlaggedUpdates, "+FLAGS (\\Flagged)");
|
|
sendUpdate(mUnflaggedUpdates, "-FLAGS (\\Flagged)");
|
|
// Delete the updates now
|
|
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
|
|
for (Long id: mUpdatedIds) {
|
|
ops.add(ContentProviderOperation.newDelete(
|
|
ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
|
|
}
|
|
applyBatch(ops);
|
|
}
|
|
}
|
|
|
|
private void sendUpdate(Stack<ServerUpdate> updates, String command) throws IOException {
|
|
// First, generate the appropriate String
|
|
while (!updates.isEmpty()) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0; i < 20 && !updates.empty(); i++) {
|
|
ServerUpdate update = updates.pop();
|
|
if (i != 0) {
|
|
sb.append(',');
|
|
}
|
|
sb.append(update.serverId);
|
|
}
|
|
String tag =
|
|
writeCommand(mConnection.writer, "uid store " + sb.toString() + " " + command);
|
|
if (!readResponse(mConnection.reader, tag, "STORE").equals(IMAP_OK)) {
|
|
errorLog("Server flag update failed?");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void copyMessage(int serverId, Mailbox mailbox) throws IOException {
|
|
String tag = writeCommand(mConnection.writer, "uid copy " + serverId + " \"" +
|
|
encodeFolderName(mailbox.mServerId) + "\"");
|
|
if (readResponse(mConnection.reader, tag, "COPY").equals(IMAP_OK)) {
|
|
tag = writeCommand(mConnection.writer, "uid store " + serverId + " +FLAGS(\\Deleted)");
|
|
if (readResponse(mConnection.reader, tag, "STORE").equals(IMAP_OK)) {
|
|
tag = writeCommand(mConnection.writer, "expunge");
|
|
readResponse(mConnection.reader, tag, "expunge");
|
|
}
|
|
} else {
|
|
errorLog("Server copy failed?");
|
|
}
|
|
}
|
|
|
|
private void saveNewMessages (ArrayList<Message> msgList) {
|
|
// Cursor dc = getLocalDeletedCursor();
|
|
// ArrayList<Integer> dl = new ArrayList<Integer>();
|
|
// boolean newDeletions = false;
|
|
// try {
|
|
// if (dc.moveToFirst()) {
|
|
// do {
|
|
// dl.add(dc.getInt(Email.UID_COLUMN));
|
|
// newDeletions = true;
|
|
// } while (dc.moveToNext());
|
|
// }
|
|
// } finally {
|
|
// dc.close();
|
|
// }
|
|
|
|
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
|
|
for (Message msg: msgList) {
|
|
//if (newDeletions && dl.contains(msg.uid)) {
|
|
// userLog("PHEW! Didn't save deleted message with uid: " + msg.uid);
|
|
// continue;
|
|
//}
|
|
msg.addSaveOps(ops);
|
|
}
|
|
applyBatch(ops);
|
|
}
|
|
|
|
private String readTextPart (ImapInputStream in, String tag, Attachment att, boolean lastPart)
|
|
throws IOException {
|
|
String res = in.readLine();
|
|
|
|
int bstart = res.indexOf("body[");
|
|
if (bstart < 0)
|
|
bstart = res.indexOf("BODY[");
|
|
if (bstart < 0)
|
|
return "";
|
|
int bend = res.indexOf(']', bstart);
|
|
if (bend < 0)
|
|
return "";
|
|
|
|
//String charset = getCharset(thisLoc);
|
|
boolean qp = att.mEncoding.equalsIgnoreCase("quoted-printable");
|
|
|
|
int br = res.indexOf('{');
|
|
if (br > 0) {
|
|
Parser p = new Parser(res, br + 1);
|
|
int length = p.parseInteger();
|
|
int len = length;
|
|
byte[] buf = new byte[len];
|
|
int offs = 0;
|
|
while (len > 0) {
|
|
int rd = in.read(buf, offs, len);
|
|
offs += rd;
|
|
len -= rd;
|
|
}
|
|
|
|
if (qp) {
|
|
length = QuotedPrintable.decode(buf, length);
|
|
}
|
|
|
|
if (lastPart) {
|
|
String line = in.readLine();
|
|
if (!line.endsWith(")")) {
|
|
userLog("Bad text part?");
|
|
throw new IOException();
|
|
}
|
|
line = in.readLine();
|
|
if (!line.startsWith(tag)) {
|
|
userLog("Bad text part?");
|
|
throw new IOException();
|
|
}
|
|
}
|
|
return new String(buf, 0, length, Charset.forName("UTF8"));
|
|
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private Thread mBodyThread;
|
|
private Connection mConnection;
|
|
|
|
private void parseBodystructure (Message msg, Parser p, String level, int cnt,
|
|
ArrayList<Attachment> viewables, ArrayList<Attachment> attachments) {
|
|
if (p.peekChar() == '(') {
|
|
// Multipart variant
|
|
while (true) {
|
|
String ps = p.parseList();
|
|
if (ps == null)
|
|
break;
|
|
parseBodystructure(msg,
|
|
new Parser(ps), level + ((level.length() > 0) ? '.' : "") + cnt, 1,
|
|
viewables, attachments);
|
|
cnt++;
|
|
}
|
|
// Multipart type (MIXED/ALTERNATIVE/RELATED)
|
|
String mp = p.parseString();
|
|
userLog("Multipart: " + mp);
|
|
} else {
|
|
boolean attachment = true;
|
|
String fileName = "";
|
|
|
|
// Here's an actual part...
|
|
// mime type
|
|
String type = p.parseString().toLowerCase();
|
|
// mime subtype
|
|
String sub = p.parseString().toLowerCase();
|
|
// parameter list or NIL
|
|
String paramList = p.parseList();
|
|
if (paramList == null)
|
|
p.parseAtom();
|
|
else {
|
|
Parser pp = new Parser(paramList);
|
|
String param;
|
|
while ((param = pp.parseString()) != null) {
|
|
String val = pp.parseString();
|
|
if (param.equalsIgnoreCase("name")) {
|
|
fileName = val;
|
|
} else if (param.equalsIgnoreCase("charset")) {
|
|
// TODO: Do we need to handle this?
|
|
}
|
|
}
|
|
}
|
|
// contentId
|
|
String contentId = p.parseString();
|
|
if (contentId != null) {
|
|
// Must remove the angle-bracket pair
|
|
contentId = contentId.substring(1, contentId.length() - 1);
|
|
fileName = "";
|
|
}
|
|
|
|
// contentName
|
|
p.parseString();
|
|
// encoding
|
|
String encoding = p.parseString().toLowerCase();
|
|
// length
|
|
Integer length = p.parseInteger();
|
|
String lvl = level.length() > 0 ? level : String.valueOf(cnt);
|
|
|
|
// body MD5
|
|
p.parseStringOrAtom();
|
|
|
|
// disposition
|
|
paramList = p.parseList();
|
|
if (paramList != null) {
|
|
//A parenthesized list, consisting of a disposition type
|
|
//string, followed by a parenthesized list of disposition
|
|
//attribute/value pairs as defined in [DISPOSITION].
|
|
Parser pp = new Parser(paramList);
|
|
String param;
|
|
while ((param = pp.parseString()) != null) {
|
|
String val = pp.parseString();
|
|
if (param.equalsIgnoreCase("name") || param.equalsIgnoreCase("filename")) {
|
|
fileName = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't waste time with Microsoft foolishness
|
|
if (!sub.equals("ms-tnef")) {
|
|
Attachment att = new Attachment();
|
|
att.mLocation = lvl;
|
|
att.mMimeType = type + "/" + sub;
|
|
att.mSize = length;
|
|
att.mFileName = fileName;
|
|
att.mEncoding = encoding;
|
|
att.mContentId = contentId;
|
|
// TODO: charset?
|
|
|
|
if ((!type.startsWith("text")) && attachment) {
|
|
//msg.encoding |= Email.ENCODING_HAS_ATTACHMENTS;
|
|
attachments.add(att);
|
|
} else {
|
|
viewables.add(att);
|
|
}
|
|
|
|
userLog("Part " + lvl + ": " + type + "/" + sub);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private void fetchMessageData(Connection conn, Cursor c) throws IOException {
|
|
for (;;) {
|
|
try {
|
|
if (c == null) {
|
|
c = mResolver.query(Message.CONTENT_URI, Message.CONTENT_PROJECTION,
|
|
MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_UNLOADED, null,
|
|
MessageColumns.TIMESTAMP + " desc");
|
|
if (c == null || c.getCount() == 0) {
|
|
return;
|
|
}
|
|
}
|
|
while (c.moveToNext()) {
|
|
// Parse the message's bodystructure
|
|
Message msg = new Message();
|
|
msg.restore(c);
|
|
ArrayList<Attachment> viewables = new ArrayList<Attachment>();
|
|
ArrayList<Attachment> attachments = new ArrayList<Attachment>();
|
|
parseBodystructure(msg, new Parser(msg.mSyncData), "", 1, viewables,
|
|
attachments);
|
|
ContentValues values = new ContentValues();
|
|
values.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
|
|
// Save the attachments...
|
|
for (Attachment att: attachments) {
|
|
att.mAccountKey = mAccount.mId;
|
|
att.mMessageKey = msg.mId;
|
|
att.save(mContext);
|
|
}
|
|
// Whether or not we have attachments
|
|
values.put(MessageColumns.FLAG_ATTACHMENT, !attachments.isEmpty());
|
|
// Get the viewables
|
|
Attachment textViewable = null;
|
|
for (Attachment viewable: viewables) {
|
|
String mimeType = viewable.mMimeType;
|
|
if ("text/html".equalsIgnoreCase(mimeType)) {
|
|
textViewable = viewable;
|
|
} else if ("text/plain".equalsIgnoreCase(mimeType) &&
|
|
textViewable == null) {
|
|
textViewable = viewable;
|
|
}
|
|
}
|
|
if (textViewable != null) {
|
|
// For now, just get single viewable
|
|
String tag = writeCommand(conn.writer,
|
|
"uid fetch " + msg.mServerId + " body.peek[" +
|
|
textViewable.mLocation + "]<0.200000>");
|
|
String text = readTextPart(conn.reader, tag, textViewable, true);
|
|
userLog("Viewable " + textViewable.mMimeType + ", len = " + text.length());
|
|
// Save it away
|
|
Body body = new Body();
|
|
if (textViewable.mMimeType.equalsIgnoreCase("text/html")) {
|
|
body.mHtmlContent = text;
|
|
} else {
|
|
body.mTextContent = text;
|
|
}
|
|
body.mMessageKey = msg.mId;
|
|
body.save(mContext);
|
|
values.put(MessageColumns.SNIPPET,
|
|
TextUtilities.makeSnippetFromHtmlText(text));
|
|
} else {
|
|
userLog("No viewable?");
|
|
values.putNull(MessageColumns.SNIPPET);
|
|
}
|
|
mResolver.update(ContentUris.withAppendedId(
|
|
Message.CONTENT_URI, msg.mId), values, null, null);
|
|
}
|
|
} finally {
|
|
if (c != null) {
|
|
c.close();
|
|
c = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void fetchMessageData () throws IOException {
|
|
// If we're already loading messages on another thread, there's nothing to do
|
|
if (mBodyThread != null) return;
|
|
HostAuth hostAuth =
|
|
HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
|
|
if (hostAuth == null) return;
|
|
// Find messages to load, if any
|
|
final Cursor unloaded = mResolver.query(Message.CONTENT_URI, Message.CONTENT_PROJECTION,
|
|
MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_UNLOADED, null,
|
|
MessageColumns.TIMESTAMP + " desc");
|
|
int cnt = unloaded.getCount();
|
|
// If there aren't any, we're done
|
|
if (cnt > 0) {
|
|
userLog("Found " + unloaded.getCount() + " messages requiring fetch");
|
|
// If we have more than one, try a second thread
|
|
// Some servers may not allow this, so we fall back to loading text on the main thread
|
|
if (cnt > 1) {
|
|
final Connection conn = connectAndLogin(hostAuth, "body");
|
|
if (conn.status == EXIT_DONE) {
|
|
mBodyThread =
|
|
new Thread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
fetchMessageData(conn, unloaded);
|
|
conn.socket.close();
|
|
} catch (IOException e) {
|
|
} finally {
|
|
mBodyThread = null;
|
|
}
|
|
}});
|
|
mBodyThread.start();
|
|
} else {
|
|
fetchMessageData(mConnection, unloaded);
|
|
}
|
|
} else {
|
|
fetchMessageData(mConnection, unloaded);
|
|
}
|
|
}
|
|
}
|
|
|
|
void readFolderList () throws IOException {
|
|
String tag = writeCommand(mWriter, "list \"\" *");
|
|
String line;
|
|
char dchar = '/';
|
|
|
|
userLog("Loading folder list...");
|
|
|
|
ArrayList<String> parentList = new ArrayList<String>();
|
|
ArrayList<Mailbox> mailboxList = new ArrayList<Mailbox>();
|
|
while (true) {
|
|
line = mReader.readLine();
|
|
userLog(line);
|
|
if (line.startsWith(tag)) {
|
|
// Done reading folder list
|
|
break;
|
|
} else {
|
|
Parser p = new Parser(line, 2);
|
|
String cmd = p.parseAtom();
|
|
if (cmd.equalsIgnoreCase("list")) {
|
|
@SuppressWarnings("unused")
|
|
String props = p.parseListOrNil();
|
|
String delim = p.parseString();
|
|
if (delim == null)
|
|
delim = "~";
|
|
if (delim.length() == 1)
|
|
dchar = delim.charAt(0);
|
|
String serverId = p.parseStringOrAtom();
|
|
int lastDelim = serverId.lastIndexOf(delim);
|
|
String displayName;
|
|
String parentName;
|
|
if (lastDelim > 0) {
|
|
displayName = serverId.substring(lastDelim + 1);
|
|
parentName = serverId.substring(0, lastDelim);
|
|
} else {
|
|
displayName = serverId;
|
|
parentName = null;
|
|
|
|
}
|
|
Mailbox m = new Mailbox();
|
|
m.mDisplayName = displayName;
|
|
m.mAccountKey = mAccount.mId;
|
|
m.mServerId = serverId;
|
|
if (parentName != null && !parentList.contains(parentName)) {
|
|
parentList.add(parentName);
|
|
}
|
|
m.mFlagVisible = true;
|
|
m.mParentServerId = parentName;
|
|
m.mDelimiter = dchar;
|
|
m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
|
|
mailboxList.add(m);
|
|
} else {
|
|
// WTF
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Use narrower projection
|
|
Cursor c = mResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
|
|
Mailbox.ACCOUNT_KEY + "=?", new String[] {Long.toString(mAccount.mId)},
|
|
MailboxColumns.SERVER_ID + " asc");
|
|
if (c == null) return;
|
|
int cnt = c.getCount();
|
|
String[] serverIds = new String[cnt];
|
|
long[] uidvals = new long[cnt];
|
|
long[] ids = new long[cnt];
|
|
int i = 0;
|
|
|
|
try {
|
|
if (c.moveToFirst()) {
|
|
// Get arrays of information about existing mailboxes in account
|
|
do {
|
|
serverIds[i] = c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN);
|
|
uidvals[i] = c.getLong(Mailbox.CONTENT_SYNC_KEY_COLUMN);
|
|
ids[i] = c.getLong(Mailbox.CONTENT_ID_COLUMN);
|
|
i++;
|
|
} while (c.moveToNext());
|
|
}
|
|
} finally {
|
|
c.close();
|
|
}
|
|
|
|
ArrayList<Mailbox> addList = new ArrayList<Mailbox>();
|
|
|
|
for (Mailbox m: mailboxList) {
|
|
int loc = Arrays.binarySearch(serverIds, m.mServerId);
|
|
if (loc >= 0) {
|
|
// It exists
|
|
if (uidvals[loc] == 0) {
|
|
// Good enough; a match that we've never visited!
|
|
// Mark this as touched (-1)...
|
|
uidvals[loc] = -1;
|
|
} else {
|
|
// Ok, now we need to see if this is the SAME mailbox...
|
|
// For now, assume it is; move on
|
|
// TODO: There's a problem if you've 1) visited this box and 2) another box now
|
|
// has its name, but how likely is that??
|
|
uidvals[loc] = -1;
|
|
}
|
|
} else {
|
|
// We don't know about this mailbox, so we'll add it...
|
|
// BUT must see if it's a rename of one we've visited!
|
|
addList.add(m);
|
|
}
|
|
}
|
|
|
|
// TODO: Flush this list every N (100?) in case there are zillions
|
|
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
|
|
try {
|
|
for (i = 0; i < cnt; i++) {
|
|
String name = serverIds[i];
|
|
long uidval = uidvals[i];
|
|
// -1 means matched; ignore
|
|
// 0 means unmatched and never before seen
|
|
// > 0 means unmatched and HAS been seen. must find mWriter why
|
|
// TODO: Get rid of "Outbox"
|
|
if (uidval == 0 && !name.equals("Outbox") &&
|
|
!name.equalsIgnoreCase(INBOX_SERVER_NAME)) {
|
|
// Ok, here's one we've never visited and it's not in the new list
|
|
ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId(
|
|
Mailbox.CONTENT_URI, ids[i])).build());
|
|
userLog("Deleting unseen mailbox; no match: " + name);
|
|
} else if (uidval > 0 && !name.equalsIgnoreCase(INBOX_SERVER_NAME)) {
|
|
boolean found = false;
|
|
for (Mailbox m : addList) {
|
|
tag = writeCommand(mWriter, "status \"" + m.mServerId + "\" (UIDVALIDITY)");
|
|
if (readResponse(mReader, tag, "STATUS").equals(IMAP_OK)) {
|
|
String str = mImapResponse.get(0).toLowerCase();
|
|
int idx = str.indexOf("uidvalidity");
|
|
long num = readLong(str, idx + 12);
|
|
if (uidval == num) {
|
|
// try {
|
|
// // This is a renamed mailbox...
|
|
// c = Mailbox.getCursorWhere(mDatabase, "account=" + mAccount.id + " and serverName=?", name);
|
|
// if (c != null && c.moveToFirst()) {
|
|
// Mailbox existing = Mailbox.restoreFromCursor(c);
|
|
// userLog("Renaming existing mailbox: " + existing.mServerId + " to: " + m.mServerId);
|
|
// existing.mDisplayName = m.mDisplayName;
|
|
// existing.mServerId = m.mServerId;
|
|
// m.mHierarchicalName = m.mServerId;
|
|
// existing.mParentServerId = m.mParentServerId;
|
|
// existing.mFlags = m.mFlags;
|
|
// existing.save(mDatabase);
|
|
// // Mark this so that we don't save it below
|
|
// m.mServerId = null;
|
|
// }
|
|
// } finally {
|
|
// if (c != null) {
|
|
// c.close();
|
|
// }
|
|
// }
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
// There's no current mailbox with this uidval, so delete.
|
|
ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId(
|
|
Mailbox.CONTENT_URI, ids[i])).build());
|
|
userLog("Deleting uidval mailbox; no match: " + name);
|
|
}
|
|
}
|
|
}
|
|
for (Mailbox m : addList) {
|
|
String serverId = m.mServerId;
|
|
if (serverId == null)
|
|
continue;
|
|
if (!serverId.equalsIgnoreCase(INBOX_SERVER_NAME)
|
|
&& !serverId.equalsIgnoreCase("Outbox")) {
|
|
m.mHierarchicalName = m.mServerId;
|
|
//*** For now, use Mail. We need a way to select the others...
|
|
m.mType = Mailbox.TYPE_MAIL;
|
|
ops.add(ContentProviderOperation.newInsert(
|
|
Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
|
|
userLog("Adding new mailbox: " + m.mServerId);
|
|
}
|
|
}
|
|
|
|
applyBatch(ops);
|
|
// Fixup parent stuff, flags...
|
|
MailboxUtilities.fixupUninitializedParentKeys(mContext,
|
|
Mailbox.ACCOUNT_KEY + "=" + mAccount.mId);
|
|
} finally {
|
|
SyncManager.kick("folder list");
|
|
}
|
|
// TODO: Make sure UI is updated
|
|
}
|
|
|
|
public int getDepth (String name, char delim) {
|
|
int depth = 0;
|
|
int last = -1;
|
|
while (true) {
|
|
last = name.indexOf(delim, last + 1);
|
|
if (last < 0)
|
|
return depth;
|
|
depth++;
|
|
}
|
|
}
|
|
|
|
|
|
private void applyBatch(ArrayList<ContentProviderOperation> ops) {
|
|
try {
|
|
mResolver.applyBatch(EmailContent.AUTHORITY, ops);
|
|
} catch (RemoteException e) {
|
|
// Nothing to be done
|
|
} catch (OperationApplicationException e) {
|
|
// These operations are legal; this can't really happen
|
|
}
|
|
}
|
|
|
|
private void processServerDeletes(ArrayList<Integer> deleteList) {
|
|
int cnt = deleteList.size();
|
|
if (cnt > 0) {
|
|
ArrayList<ContentProviderOperation> ops =
|
|
new ArrayList<ContentProviderOperation>();
|
|
for (int i = 0; i < cnt; i++) {
|
|
MAILBOX_SERVER_ID_ARGS[1] = Long.toString(deleteList.get(i));
|
|
Builder b = ContentProviderOperation.newDelete(
|
|
Message.SYNCED_SELECTION_CONTENT_URI);
|
|
b.withSelection(MessageColumns.MAILBOX_KEY + "=? AND " +
|
|
SyncColumns.SERVER_ID + "=?", MAILBOX_SERVER_ID_ARGS);
|
|
ops.add(b.build());
|
|
}
|
|
applyBatch(ops);
|
|
}
|
|
}
|
|
|
|
private void processServerUpdates(ArrayList<Integer> deleteList, ContentValues values) {
|
|
int cnt = deleteList.size();
|
|
if (cnt > 0) {
|
|
ArrayList<ContentProviderOperation> ops =
|
|
new ArrayList<ContentProviderOperation>();
|
|
for (int i = 0; i < cnt; i++) {
|
|
MAILBOX_SERVER_ID_ARGS[1] = Long.toString(deleteList.get(i));
|
|
Builder b = ContentProviderOperation.newUpdate(
|
|
Message.SYNCED_SELECTION_CONTENT_URI);
|
|
b.withSelection(MessageColumns.MAILBOX_KEY + "=? AND " +
|
|
SyncColumns.SERVER_ID + "=?", MAILBOX_SERVER_ID_ARGS);
|
|
b.withValues(values);
|
|
ops.add(b.build());
|
|
}
|
|
applyBatch(ops);
|
|
}
|
|
}
|
|
|
|
private static class Reconciled {
|
|
ArrayList<Integer> insert;
|
|
ArrayList<Integer> delete;
|
|
|
|
Reconciled (ArrayList<Integer> ins, ArrayList<Integer> del) {
|
|
insert = ins;
|
|
delete = del;
|
|
}
|
|
}
|
|
|
|
// Arrays must be sorted beforehand
|
|
public Reconciled reconcile (String name, int[] deviceList, int[] serverList) {
|
|
ArrayList<Integer> loadList = new ArrayList<Integer>();
|
|
ArrayList<Integer> deleteList = new ArrayList<Integer>();
|
|
int soff = 0;
|
|
int doff = 0;
|
|
int scnt = serverList.length;
|
|
int dcnt = deviceList.length;
|
|
|
|
while (scnt > 0 || dcnt > 0) {
|
|
if (scnt == 0) {
|
|
for (; dcnt > 0; dcnt--)
|
|
deleteList.add(deviceList[doff++]);
|
|
break;
|
|
} else if (dcnt == 0) {
|
|
for (; scnt > 0; scnt--)
|
|
loadList.add(serverList[soff++]);
|
|
break;
|
|
}
|
|
int s = serverList[soff++];
|
|
int d = deviceList[doff++];
|
|
scnt--;
|
|
dcnt--;
|
|
if (s == d) {
|
|
continue;
|
|
} else if (s > d) {
|
|
deleteList.add(d);
|
|
scnt++;
|
|
soff--;
|
|
} else if (d > s) {
|
|
loadList.add(s);
|
|
dcnt++;
|
|
doff--;
|
|
}
|
|
}
|
|
|
|
userLog("Reconciler " + name + "-> Insert: " + loadList.size() +
|
|
", Delete: " + deleteList.size());
|
|
return new Reconciled(loadList, deleteList);
|
|
}
|
|
|
|
private static final String[] UID_PROJECTION = new String[] {SyncColumns.SERVER_ID};
|
|
public int[] getUidList (String andClause) {
|
|
int offs = 0;
|
|
String ac = MessageColumns.MAILBOX_KEY + "=?";
|
|
if (andClause != null) {
|
|
ac = ac + andClause;
|
|
}
|
|
Cursor c = mResolver.query(Message.CONTENT_URI, UID_PROJECTION,
|
|
ac, new String[] {Long.toString(mMailboxId)}, SyncColumns.SERVER_ID);
|
|
if (c != null) {
|
|
try {
|
|
int[] uids = new int[c.getCount()];
|
|
if (c.moveToFirst()) {
|
|
do {
|
|
uids[offs++] = c.getInt(0);
|
|
} while (c.moveToNext());
|
|
return uids;
|
|
}
|
|
} finally {
|
|
c.close();
|
|
}
|
|
}
|
|
return new int[0];
|
|
}
|
|
|
|
public int[] getUnreadUidList () {
|
|
return getUidList(" and " + Message.FLAG_READ + "=0");
|
|
}
|
|
|
|
public int[] getFlaggedUidList () {
|
|
return getUidList(" and " + Message.FLAG_FAVORITE + "!=0");
|
|
}
|
|
|
|
private void reconcileState(int[] deviceList, String since, String flag, String search,
|
|
String column, boolean sense) throws IOException {
|
|
int[] serverList;
|
|
Parser p;
|
|
String msgs;
|
|
String tag = writeCommand(mWriter, "uid search undeleted " + search + " since " + since);
|
|
if (readResponse(mReader, tag, "SEARCH").equals(IMAP_OK)) {
|
|
if (mImapResponse.isEmpty()) {
|
|
serverList = new int[0];
|
|
} else {
|
|
msgs = mImapResponse.get(0);
|
|
p = new Parser(msgs, 8);
|
|
serverList = p.gatherInts();
|
|
Arrays.sort(serverList);
|
|
}
|
|
Reconciled r = reconcile(flag, deviceList, serverList);
|
|
ContentValues values = new ContentValues();
|
|
values.put(column, sense);
|
|
processServerUpdates(r.delete, values);
|
|
values.put(column, !sense);
|
|
processServerUpdates(r.insert, values);
|
|
}
|
|
}
|
|
|
|
private ArrayList<String> getTokens(String str) {
|
|
ArrayList<String> tokens = new ArrayList<String>();
|
|
Parser p = new Parser(str);
|
|
while(true) {
|
|
String capa = p.parseAtom();
|
|
if (capa == null) {
|
|
break;
|
|
}
|
|
tokens.add(capa);
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
public static class Connection {
|
|
Socket socket;
|
|
int status;
|
|
ImapInputStream reader;
|
|
BufferedWriter writer;
|
|
}
|
|
|
|
private String mUserAgent;
|
|
|
|
private Connection connectAndLogin(HostAuth hostAuth, String name) {
|
|
Connection conn = new Connection();
|
|
Socket socket;
|
|
try {
|
|
socket = getSocket(hostAuth);
|
|
socket.setSoTimeout(SOCKET_TIMEOUT);
|
|
userLog(">>> IMAP CONNECTION SUCCESSFUL: " + name);
|
|
|
|
ImapInputStream reader = new ImapInputStream(socket.getInputStream());
|
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
|
|
socket.getOutputStream()));
|
|
// Get welcome string
|
|
reader.readLine();
|
|
|
|
String tag = writeCommand(writer, "CAPABILITY");
|
|
if (readResponse(reader, tag, "CAPABILITY").equals(IMAP_OK)) {
|
|
// If CAPABILITY
|
|
if (!mImapResponse.isEmpty()) {
|
|
String capa = mImapResponse.get(0).toLowerCase();
|
|
ArrayList<String> tokens = getTokens(capa);
|
|
if (tokens.contains("starttls")) {
|
|
// Handle STARTTLS
|
|
userLog("[Supports STARTTLS]");
|
|
}
|
|
if (tokens.contains("id")) {
|
|
String hostAddress = hostAuth.mAddress;
|
|
// Never send ID to *.secureserver.net
|
|
// Hackish, yes, but we've been doing this for years... :-(
|
|
if (!hostAddress.toLowerCase().endsWith(".secureserver.net")) {
|
|
// Assign user-agent string (for RFC2971 ID command)
|
|
if (mUserAgent == null) {
|
|
mUserAgent = ImapId.getImapId(mContext, hostAuth.mLogin,
|
|
hostAddress, null);
|
|
}
|
|
tag = writeCommand(writer, "ID (" + mUserAgent + ")");
|
|
// We learn nothing useful from the response
|
|
readResponse(reader, tag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tag = writeCommand(writer,
|
|
"login " + hostAuth.mLogin + ' ' + hostAuth.mPassword);
|
|
if (!readResponse(reader, tag).equals(IMAP_OK)) {
|
|
conn.status = EXIT_LOGIN_FAILURE;
|
|
} else {
|
|
conn.socket = socket;
|
|
conn.reader = reader;
|
|
conn.writer = writer;
|
|
conn.status = EXIT_DONE;
|
|
userLog(">>> LOGGED IN: " + name);
|
|
if (mMailboxName != null) {
|
|
tag = writeCommand(conn.writer, "select \"" + encodeFolderName(mMailboxName) +
|
|
'\"');
|
|
if (!readResponse(conn.reader, tag).equals(IMAP_OK)) {
|
|
// Select failed
|
|
userLog("Select failed?");
|
|
conn.status = EXIT_EXCEPTION;
|
|
} else {
|
|
userLog(">>> SELECTED");
|
|
}
|
|
}
|
|
}
|
|
} catch (CertificateValidationException e) {
|
|
conn.status = EXIT_LOGIN_FAILURE;
|
|
} catch (IOException e) {
|
|
conn.status = EXIT_IO_ERROR;
|
|
}
|
|
return conn;
|
|
}
|
|
|
|
private void setMailboxSyncStatus(long id, int status) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(Mailbox.UI_SYNC_STATUS, status);
|
|
// Make sure we're always showing a "success" value. A failure wouldn't get set here, but
|
|
// rather via SyncService.done()
|
|
values.put(Mailbox.UI_LAST_SYNC_RESULT, Mailbox.LAST_SYNC_RESULT_SUCCESS);
|
|
mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
|
|
}
|
|
|
|
private void idle() throws IOException {
|
|
mIsIdle = true;
|
|
mThread.setName(mMailboxName + ":IDLE[" + mAccount.mDisplayName + "]");
|
|
userLog("Entering idle...");
|
|
String tag = writeCommand(mWriter, "idle");
|
|
|
|
try {
|
|
while (true) {
|
|
String resp = mReader.readLine();
|
|
if (resp.startsWith("+"))
|
|
break;
|
|
// Remember to handle untagged responses here (sigh, and elsewhere)
|
|
if (resp.startsWith("* "))
|
|
handleUntagged(resp);
|
|
else {
|
|
userLog("Error in IDLE response: " + resp);
|
|
//*** How to handle this?
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Server has accepted IDLE
|
|
long idleStartTime = System.currentTimeMillis();
|
|
|
|
// Let the socket time out a minute after we expect to terminate it ourselves
|
|
mSocket.setSoTimeout(IDLE_ASLEEP_MILLIS + (1*MINS));
|
|
// Say we're no longer syncing (turn off indeterminate progress in the UI)
|
|
setMailboxSyncStatus(mMailboxId, UIProvider.SyncStatus.NO_SYNC);
|
|
// Set an alarm for one minute before our timeout our expected IDLE time
|
|
Imap2SyncManager.runAsleep(mMailboxId, IDLE_ASLEEP_MILLIS);
|
|
|
|
while (true) {
|
|
String line = null;
|
|
try {
|
|
line = mReader.readLine();
|
|
userLog(line);
|
|
} catch (SocketTimeoutException e) {
|
|
userLog("Socket timeout");
|
|
} finally {
|
|
Imap2SyncManager.runAwake(mMailboxId);
|
|
// Say we're syncing again
|
|
setMailboxSyncStatus(mMailboxId, UIProvider.SyncStatus.BACKGROUND_SYNC);
|
|
}
|
|
if (line == null || line.startsWith("* ")) {
|
|
boolean finish = (line == null) ? true : handleUntagged(line);
|
|
if (!finish) {
|
|
long timeSinceIdle =
|
|
System.currentTimeMillis() - idleStartTime;
|
|
// If we're nearing the end of IDLE time, let's just reset the IDLE while
|
|
// we've got the processor awake
|
|
if (timeSinceIdle > IDLE_ASLEEP_MILLIS - (2*MINS)) {
|
|
userLog("Time to reset IDLE...");
|
|
finish = true;
|
|
}
|
|
}
|
|
if (finish) {
|
|
mWriter.write("DONE\r\n");
|
|
mWriter.flush();
|
|
}
|
|
} else if (line.startsWith(tag)) {
|
|
Parser p = new Parser(line, tag.length() - 1);
|
|
mImapResult = p.parseAtom();
|
|
mIsIdle = false;
|
|
break;
|
|
}
|
|
}
|
|
} finally {
|
|
// We might have left IDLE due to an exception
|
|
if (mSocket != null) {
|
|
// Reset the standard timeout
|
|
mSocket.setSoTimeout(20 * 1000);
|
|
}
|
|
mIsIdle = false;
|
|
mThread.setName(mMailboxName + "[" + mAccount.mDisplayName + "]");
|
|
}
|
|
}
|
|
|
|
// Upload sent messages to server
|
|
|
|
// void foo() {
|
|
// Cursor c = ServerUploads.getCursorWhere(mDatabase, "account=" + mAccount.id);
|
|
// ArrayList<Long> uploaded = new ArrayList<Long>();
|
|
// try {
|
|
// if (c.moveToFirst()) {
|
|
// do{
|
|
// Mailbox m = Mailbox.restoreFromId(mDatabase, c.getLong(ServerUploads.TO_MAILBOX_COLUMN));
|
|
// String fn = c.getString(ServerUploads.FILENAME_COLUMN);
|
|
// if (m != null) {
|
|
// FileInputStream fi = null;
|
|
// try {
|
|
// fi = mContext.openFileInput(fn);
|
|
// } catch (Exception e) {
|
|
// logException(e);
|
|
// }
|
|
// if (fi != null) {
|
|
// BufferedInputStream bin = new BufferedInputStream(fi);
|
|
// mWriter.flush();
|
|
// BufferedOutputStream bos = new BufferedOutputStream(mSocket.getOutputStream());
|
|
// int len = fi.available();
|
|
// byte[] buf;
|
|
// try {
|
|
// tag = writeCommand(mWriter, "append \"" + m.serverName + "\" (\\seen) {" + len + '}');
|
|
// String line = in.readLine();
|
|
// buf = new byte[CHUNK_SIZE];
|
|
// if (line.startsWith("+")) {
|
|
// userLog("append response: " + line);
|
|
//
|
|
// while (len > 0) {
|
|
// int rlen = (len > CHUNK_SIZE) ? CHUNK_SIZE : len;
|
|
// int rd = bin.read(buf, 0, rlen);
|
|
// if (rd > 0) {
|
|
// bos.write(buf, 0, rd);
|
|
// } else if (rd < 0)
|
|
// break;
|
|
// len -= rd;
|
|
// }
|
|
//
|
|
// bos.flush();
|
|
// mWriter.write("\r\n");
|
|
// mWriter.flush();
|
|
// bin.close();
|
|
// if (readResponse(in, tag).equals(IMAP_OK)) {
|
|
// uploaded.add(c.getLong(ServerUploads.ID_COLUMN));
|
|
// File f = mContext.getFileStreamPath(fn);
|
|
// if (f.delete()) {
|
|
// userLog("Upload file deleted: " + fn);
|
|
// }
|
|
// } else {
|
|
// userLog("Append failed?");
|
|
// uploaded.add(c.getLong(ServerUploads.ID_COLUMN));
|
|
// }
|
|
// } else {
|
|
// userLog("Append failed: " + line);
|
|
// uploaded.add(c.getLong(ServerUploads.ID_COLUMN));
|
|
// }
|
|
// } catch (Exception e) {
|
|
// logException(e);
|
|
// uploaded.add(c.getLong(ServerUploads.ID_COLUMN));
|
|
// }
|
|
//
|
|
// buf = null;
|
|
// if (m.name.equals(Mailbox.DRAFTS_NAME))
|
|
// MailService.serviceRequest(m.id, 3000L);
|
|
// } else {
|
|
// userLog("Can't find file to upload, deleting upload record: " + fn);
|
|
// uploaded.add(c.getLong(ServerUploads.ID_COLUMN));
|
|
// }
|
|
// }
|
|
// } while (c.moveToNext());
|
|
// }
|
|
// } finally {
|
|
// // Delete the upload records for those completed
|
|
// for (Long id: uploaded) {
|
|
// ServerUploads.deleteById(mDatabase, id);
|
|
// }
|
|
// c.close();
|
|
// }
|
|
// }
|
|
|
|
private void processRequests() throws IOException {
|
|
while (!mRequestQueue.isEmpty()) {
|
|
Request req = mRequestQueue.peek();
|
|
|
|
// Our two request types are PartRequest (loading attachment) and
|
|
// MeetingResponseRequest (respond to a meeting request)
|
|
if (req instanceof PartRequest) {
|
|
TrafficStats.setThreadStatsTag(
|
|
TrafficFlags.getAttachmentFlags(mContext, mAccount));
|
|
new AttachmentLoader(this,
|
|
(PartRequest)req).loadAttachment(mConnection);
|
|
TrafficStats.setThreadStatsTag(
|
|
TrafficFlags.getSyncFlags(mContext, mAccount));
|
|
}
|
|
|
|
// If there's an exception handling the request, we'll throw it
|
|
// Otherwise, we remove the request
|
|
mRequestQueue.remove();
|
|
}
|
|
}
|
|
|
|
private void sync () throws IOException {
|
|
mThread = Thread.currentThread();
|
|
|
|
HostAuth hostAuth =
|
|
HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
|
|
if (hostAuth == null) return;
|
|
Connection conn = connectAndLogin(hostAuth, "main");
|
|
if (conn.status != EXIT_DONE) {
|
|
mExitStatus = conn.status;
|
|
return;
|
|
}
|
|
setConnection(conn);
|
|
|
|
// The account might have changed!!
|
|
//*** Determine how to often to do this
|
|
if (mMailboxName.equalsIgnoreCase("inbox")) {
|
|
long startTime = System.currentTimeMillis();
|
|
readFolderList();
|
|
userLog("Folder list processed in " + (System.currentTimeMillis() - startTime) +
|
|
"ms");
|
|
}
|
|
|
|
while (!mStop) {
|
|
try {
|
|
while (!mStop) {
|
|
mIsServiceRequestPending = false;
|
|
|
|
// Now, handle various requests
|
|
processRequests();
|
|
|
|
long days;
|
|
if (mMailbox.mSyncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN) {
|
|
days = 14;
|
|
} else
|
|
days = SyncWindow.toDays(mMailbox.mSyncLookback);
|
|
|
|
long time = System.currentTimeMillis() - (days*DAYS);
|
|
Date date = new Date(time);
|
|
String since = IMAP_DATE_FORMAT.format(date);
|
|
String tag = writeCommand(mWriter, "uid search undeleted since " + since);
|
|
|
|
// TODO Handle multi-line search result (google)
|
|
if (!readResponse(mReader, tag, "SEARCH").equals(IMAP_OK)) {
|
|
userLog("$$$ WHOA! Search failed? ");
|
|
}
|
|
|
|
userLog(">>> SEARCH RESULT");
|
|
int[] serverList;
|
|
String msgs;
|
|
Parser p;
|
|
if (mImapResponse.isEmpty()) {
|
|
serverList = new int[0];
|
|
} else {
|
|
msgs = mImapResponse.get(0);
|
|
//*** Magic number?
|
|
p = new Parser(msgs, 8);
|
|
serverList = p.gatherInts();
|
|
}
|
|
|
|
Arrays.sort(serverList);
|
|
int[] deviceList = getUidList(null);
|
|
Reconciled r =
|
|
reconcile("MESSAGES", deviceList, serverList);
|
|
ArrayList<Integer> loadList = r.insert;
|
|
ArrayList<Integer> deleteList = r.delete;
|
|
serverList = null;
|
|
deviceList = null;
|
|
int cnt = loadList.size();
|
|
|
|
// We load message headers 20 at a time at this point...
|
|
int idx= 1;
|
|
boolean loadedSome = false;
|
|
while (idx <= cnt) {
|
|
ArrayList<Message> tmsgList = new ArrayList<Message> ();
|
|
int tcnt = 0;
|
|
StringBuilder tsb = new StringBuilder("uid fetch ");
|
|
for (tcnt = 0; tcnt < HEADER_BATCH_COUNT && idx <= cnt; tcnt++, idx++) {
|
|
// Load most recent first
|
|
if (tcnt > 0)
|
|
tsb.append(',');
|
|
tsb.append(loadList.get(cnt - idx));
|
|
}
|
|
tsb.append(" (uid internaldate flags envelope bodystructure)");
|
|
tag = writeCommand(mWriter, tsb.toString());
|
|
if (readResponse(mReader, tag, "FETCH").equals(IMAP_OK)) {
|
|
// Create message and store
|
|
for (int j = 0; j < tcnt; j++) {
|
|
Message msg = createMessage(mImapResponse.get(j));
|
|
tmsgList.add(msg);
|
|
}
|
|
saveNewMessages(tmsgList);
|
|
}
|
|
|
|
fetchMessageData();
|
|
loadedSome = true;
|
|
}
|
|
// TODO: Use loader to watch for changes on unloaded body cursor
|
|
if (!loadedSome) {
|
|
fetchMessageData();
|
|
}
|
|
|
|
// Reflect server deletions on device; do them all at once
|
|
processServerDeletes(deleteList);
|
|
|
|
handleLocalUpdates();
|
|
|
|
handleLocalDeletes();
|
|
|
|
reconcileState(getUnreadUidList(), since, "UNREAD", "unseen",
|
|
MessageColumns.FLAG_READ, true);
|
|
reconcileState(getFlaggedUidList(), since, "FLAGGED", "flagged",
|
|
MessageColumns.FLAG_FAVORITE, false);
|
|
|
|
// We're done if not pushing...
|
|
if (mMailbox.mSyncInterval != Mailbox.CHECK_INTERVAL_PUSH) {
|
|
mExitStatus = EXIT_DONE;
|
|
return;
|
|
}
|
|
|
|
// If new requests have come in, process them
|
|
if (mIsServiceRequestPending)
|
|
continue;
|
|
|
|
idle();
|
|
}
|
|
|
|
} finally {
|
|
if (mSocket != null) {
|
|
try {
|
|
mSocket.close();
|
|
} catch (IOException e) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
// If we've been stopped, we're done
|
|
if (mStop) return;
|
|
|
|
// Whether or not we're the account mailbox
|
|
try {
|
|
if ((mMailbox == null) || (mAccount == null)) {
|
|
return;
|
|
} else {
|
|
int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
|
|
TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
|
|
|
|
// We loop because someone might have put a request in while we were syncing
|
|
// and we've missed that opportunity...
|
|
do {
|
|
if (mRequestTime != 0) {
|
|
userLog("Looping for user request...");
|
|
mRequestTime = 0;
|
|
}
|
|
if (mSyncReason >= Imap2SyncManager.SYNC_CALLBACK_START) {
|
|
try {
|
|
Imap2SyncManager.callback().syncMailboxStatus(mMailboxId,
|
|
EmailServiceStatus.IN_PROGRESS, 0);
|
|
} catch (RemoteException e1) {
|
|
// Don't care if this fails
|
|
}
|
|
}
|
|
sync();
|
|
} while (mRequestTime != 0);
|
|
}
|
|
} catch (IOException e) {
|
|
String message = e.getMessage();
|
|
userLog("Caught IOException: ", (message == null) ? "No message" : message);
|
|
mExitStatus = EXIT_IO_ERROR;
|
|
} catch (Exception e) {
|
|
userLog("Uncaught exception in EasSyncService", e);
|
|
} finally {
|
|
int status;
|
|
Imap2SyncManager.done(this);
|
|
if (!mStop) {
|
|
userLog("Sync finished");
|
|
switch (mExitStatus) {
|
|
case EXIT_IO_ERROR:
|
|
status = EmailServiceStatus.CONNECTION_ERROR;
|
|
break;
|
|
case EXIT_DONE:
|
|
status = EmailServiceStatus.SUCCESS;
|
|
ContentValues cv = new ContentValues();
|
|
cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
|
|
String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
|
|
cv.put(Mailbox.SYNC_STATUS, s);
|
|
mContext.getContentResolver().update(
|
|
ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
|
|
cv, null, null);
|
|
break;
|
|
case EXIT_LOGIN_FAILURE:
|
|
status = EmailServiceStatus.LOGIN_FAILED;
|
|
break;
|
|
default:
|
|
status = EmailServiceStatus.REMOTE_EXCEPTION;
|
|
errorLog("Sync ended due to an exception.");
|
|
break;
|
|
}
|
|
} else {
|
|
userLog("Stopped sync finished.");
|
|
status = EmailServiceStatus.SUCCESS;
|
|
}
|
|
|
|
// Send a callback (doesn't matter how the sync was started)
|
|
try {
|
|
// Unless the user specifically asked for a sync, we don't want to report
|
|
// connection issues, as they are likely to be transient. In this case, we
|
|
// simply report success, so that the progress indicator terminates without
|
|
// putting up an error banner
|
|
//***
|
|
if (mSyncReason != Imap2SyncManager.SYNC_UI_REQUEST &&
|
|
status == EmailServiceStatus.CONNECTION_ERROR) {
|
|
status = EmailServiceStatus.SUCCESS;
|
|
}
|
|
Imap2SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
|
|
} catch (RemoteException e1) {
|
|
// Don't care if this fails
|
|
}
|
|
|
|
// Make sure ExchangeService knows about this
|
|
Imap2SyncManager.kick("sync finished");
|
|
}
|
|
} catch (ProviderUnavailableException e) {
|
|
Log.e(TAG, "EmailProvider unavailable; sync ended prematurely");
|
|
}
|
|
}
|
|
|
|
private Socket getSocket(HostAuth hostAuth) throws CertificateValidationException, IOException {
|
|
Socket socket;
|
|
try {
|
|
boolean ssl = (hostAuth.mFlags & HostAuth.FLAG_SSL) != 0;
|
|
boolean trust = (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
|
|
SocketAddress socketAddress = new InetSocketAddress(hostAuth.mAddress, hostAuth.mPort);
|
|
if (ssl) {
|
|
socket = SSLUtils.getSSLSocketFactory(trust).createSocket();
|
|
} else {
|
|
socket = new Socket();
|
|
}
|
|
socket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
|
// After the socket connects to an SSL server, confirm that the hostname is as expected
|
|
if (ssl && !trust) {
|
|
verifyHostname(socket, hostAuth.mAddress);
|
|
}
|
|
} catch (SSLException e) {
|
|
errorLog(e.toString());
|
|
throw new CertificateValidationException(e.getMessage(), e);
|
|
}
|
|
return socket;
|
|
}
|
|
|
|
/**
|
|
* Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
|
|
* service but is not in the public API.
|
|
*
|
|
* Verify the hostname of the certificate used by the other end of a
|
|
* connected socket. You MUST call this if you did not supply a hostname
|
|
* to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method
|
|
* redundantly if the hostname has already been verified.
|
|
*
|
|
* <p>Wildcard certificates are allowed to verify any matching hostname,
|
|
* so "foo.bar.example.com" is verified if the peer has a certificate
|
|
* for "*.example.com".
|
|
*
|
|
* @param socket An SSL socket which has been connected to a server
|
|
* @param hostname The expected hostname of the remote server
|
|
* @throws IOException if something goes wrong handshaking with the server
|
|
* @throws SSLPeerUnverifiedException if the server cannot prove its identity
|
|
*/
|
|
private void verifyHostname(Socket socket, String hostname) throws IOException {
|
|
// The code at the start of OpenSSLSocketImpl.startHandshake()
|
|
// ensures that the call is idempotent, so we can safely call it.
|
|
SSLSocket ssl = (SSLSocket) socket;
|
|
ssl.startHandshake();
|
|
|
|
SSLSession session = ssl.getSession();
|
|
if (session == null) {
|
|
throw new SSLException("Cannot verify SSL socket without session");
|
|
}
|
|
// TODO: Instead of reporting the name of the server we think we're connecting to,
|
|
// we should be reporting the bad name in the certificate. Unfortunately this is buried
|
|
// in the verifier code and is not available in the verifier API, and extracting the
|
|
// CN & alts is beyond the scope of this patch.
|
|
if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) {
|
|
throw new SSLPeerUnverifiedException(
|
|
"Certificate hostname not useable for server: " + hostname);
|
|
}
|
|
}
|
|
}
|