More work on Imap2

* Handle sending mail and moving to sent folder
* Implement picker for sent folder
* Upload sent items to server
* Add support for "automatic" sync window
* Move some files from Email -> emailcommon
* The added files are copied directly from Email (and can be
  removed if/when Imap2 is merged back with Email)

Change-Id: I3a6a3d224826e547748be2f1b567b6294ad5db89
This commit is contained in:
Marc Blank 2012-07-18 12:52:40 -07:00
parent c992071671
commit a8b683cf3f
21 changed files with 1139 additions and 282 deletions

View File

@ -14,10 +14,8 @@
* limitations under the License.
*/
package com.android.email.mail;
package com.android.emailcommon.mail;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import java.io.IOException;
import java.io.InputStream;

View File

@ -73,6 +73,8 @@ public abstract class EmailContent {
public static final Uri PICK_TRASH_FOLDER_URI =
Uri.parse("content://" + EmailContent.AUTHORITY + "/pickTrashFolder");
public static final Uri PICK_SENT_FOLDER_URI =
Uri.parse("content://" + EmailContent.AUTHORITY + "/pickSentFolder");
public static final Uri MAILBOX_NOTIFICATION_URI =
Uri.parse("content://" + EmailContent.AUTHORITY + "/mailboxNotification");

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.email.mail.transport;
package com.android.emailcommon.utility;
import java.io.IOException;
import java.io.OutputStream;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.email.mail.transport;
package com.android.emailcommon.utility;
import java.io.FilterOutputStream;
import java.io.IOException;

View File

@ -34,6 +34,7 @@ import android.net.NetworkInfo;
import android.net.NetworkInfo.State;
import android.net.Uri;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;

View File

@ -17,7 +17,6 @@
package com.android.imap2;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallbackList;
@ -29,7 +28,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import com.android.emailcommon.Api;
import com.android.emailcommon.provider.EmailContent;
@ -212,10 +210,16 @@ public class Imap2SyncManager extends SyncManager {
return new AccountObserver(handler) {
@Override
public void newAccount(long acctId) {
// Create the Inbox for the account
Account acct = Account.restoreAccountWithId(getContext(), acctId);
// Create the Inbox for the account if it doesn't exist
Context context = getContext();
Account acct = Account.restoreAccountWithId(context, acctId);
if (acct == null) return;
long inboxId = Mailbox.findMailboxOfType(context, acctId, Mailbox.TYPE_INBOX);
if (inboxId != Mailbox.NO_MAILBOX) {
return;
}
Mailbox inbox = new Mailbox();
inbox.mDisplayName = "Inbox"; // Localize
inbox.mDisplayName = context.getString(R.string.mailbox_name_server_inbox);
inbox.mServerId = "Inbox";
inbox.mAccountKey = acct.mId;
inbox.mType = Mailbox.TYPE_INBOX;

View File

@ -24,16 +24,18 @@ import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.TrafficStats;
import android.net.Uri;
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.internet.Rfc822Output;
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.AccountColumns;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.Body;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
@ -43,11 +45,12 @@ 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.CountingOutputStream;
import com.android.emailcommon.utility.EOLConvertingOutputStream;
import com.android.emailcommon.utility.SSLUtils;
import com.android.emailcommon.utility.TextUtilities;
import com.android.emailcommon.utility.Utility;
@ -55,6 +58,7 @@ import com.android.emailsync.AbstractSyncService;
import com.android.emailsync.PartRequest;
import com.android.emailsync.Request;
import com.android.emailsync.SyncManager;
import com.android.imap2.smtp.SmtpSender;
import com.android.mail.providers.UIProvider;
import com.beetstra.jutf7.CharsetProvider;
@ -103,17 +107,18 @@ public class Imap2SyncService extends AbstractSyncService {
private static Pattern IMAP_RESPONSE_PATTERN = Pattern.compile("\\*(\\s(\\d+))?\\s(\\w+).*");
private static final int HEADER_BATCH_COUNT = 10;
private static final int HEADER_BATCH_COUNT = 20;
// 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 static final int AUTOMATIC_SYNC_WINDOW_MAX_MESSAGES = 250;
private static final int AUTOMATIC_SYNC_WINDOW_LARGE_MAILBOX = 1000;
private ContentResolver mResolver;
private int mWriterTag = 1;
private boolean mIsGmail = false;
@ -123,6 +128,7 @@ public class Imap2SyncService extends AbstractSyncService {
private ArrayList<String> mImapResponse = null;
private String mImapResult;
private String mImapErrorLine = null;
private String mImapSuccessLine = null;
private Socket mSocket = null;
private boolean mStop = false;
@ -144,10 +150,23 @@ public class Imap2SyncService extends AbstractSyncService {
private HostAuth mHostAuth;
private String mPrefix;
private long mTrashMailboxId = Mailbox.NO_MAILBOX;
private long mAccountId;
private final ArrayList<Long> mUpdatedIds = new ArrayList<Long>();
private final ArrayList<Long> mDeletedIds = new ArrayList<Long>();
private final Stack<Integer> mDeletes = new Stack<Integer>();
private final Stack<Integer> mReadUpdates = new Stack<Integer>();
private final Stack<Integer> mUnreadUpdates = new Stack<Integer>();
private final Stack<Integer> mFlaggedUpdates = new Stack<Integer>();
private final Stack<Integer> mUnflaggedUpdates = new Stack<Integer>();
public Imap2SyncService(Context _context, Mailbox _mailbox) {
super(_context, _mailbox);
mResolver = _context.getContentResolver();
if (mAccount != null) {
mAccountId = mAccount.mId;
}
MAILBOX_SERVER_ID_ARGS[0] = Long.toString(mMailboxId);
}
@ -160,8 +179,10 @@ public class Imap2SyncService extends AbstractSyncService {
mContext = _context;
mResolver = _context.getContentResolver();
mAccount = _account;
mAccountId = _account.mId;
mHostAuth = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
mPrefix = mHostAuth.mDomain;
mTrashMailboxId = Mailbox.findMailboxOfType(_context, _account.mId, Mailbox.TYPE_TRASH);
}
@Override
@ -282,8 +303,9 @@ public class Imap2SyncService extends AbstractSyncService {
out.write(cmd);
out.write("\r\n");
out.flush();
if (!cmd.startsWith("login"))
if (!cmd.startsWith("login")) {
userLog(tag + cmd);
}
return tag;
} catch (IOException e) {
userLog("IOException in writeCommand");
@ -317,20 +339,21 @@ public class Imap2SyncService extends AbstractSyncService {
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);
}
}
else if (mMailbox != null && mMailbox.mSyncKey == null || mMailbox.mSyncKey == "0") {
str = str.toLowerCase();
int idx = str.indexOf("uidvalidity");
if (idx > 0) {
// 12 = length of "uidvalidity" + 1
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);
@ -373,7 +396,9 @@ public class Imap2SyncService extends AbstractSyncService {
}
}
if (!mImapResult.equals(IMAP_OK)) {
if (mImapResult.equals(IMAP_OK)) {
mImapSuccessLine = str;
} else {
userLog("$$$ Error result = " + mImapResult);
mImapErrorLine = str;
}
@ -471,7 +496,7 @@ public class Imap2SyncService extends AbstractSyncService {
//msg.bodyId = 0;
//msg.parts = parts.toString();
msg.mAccountKey = mAccount.mId;
msg.mAccountKey = mAccountId;
msg.mFlagLoaded = Message.FLAG_LOADED_UNLOADED;
msg.mFlags = flag;
@ -620,24 +645,6 @@ public class Imap2SyncService extends AbstractSyncService {
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);
@ -676,7 +683,7 @@ public class Imap2SyncService extends AbstractSyncService {
try {
while (c.moveToNext()) {
long id = c.getLong(UPDATE_DELETE_ID_COLUMN);
mDeletes.add(new ServerUpdate(id, c.getInt(UPDATE_DELETE_SERVER_ID_COLUMN)));
mDeletes.add(c.getInt(UPDATE_DELETE_SERVER_ID_COLUMN));
mDeletedIds.add(id);
}
sendUpdate(mDeletes, "+FLAGS (\\Deleted)");
@ -758,7 +765,7 @@ public class Imap2SyncService extends AbstractSyncService {
continue;
}
ServerUpdate update = new ServerUpdate(id, serverId);
Integer update = serverId;
if (readChange) {
if (read == 1) {
mReadUpdates.add(update);
@ -796,16 +803,16 @@ public class Imap2SyncService extends AbstractSyncService {
}
}
private void sendUpdate(Stack<ServerUpdate> updates, String command) throws IOException {
private void sendUpdate(Stack<Integer> 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();
Integer update = updates.pop();
if (i != 0) {
sb.append(',');
}
sb.append(update.serverId);
sb.append(update);
}
String tag =
writeCommand(mConnection.writer, "uid store " + sb.toString() + " " + command);
@ -831,28 +838,39 @@ public class Imap2SyncService extends AbstractSyncService {
}
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();
// }
// Get the ids of updated messages in this mailbox (usually there won't be any)
Cursor c = getUpdatesCursor();
ArrayList<Integer> updatedIds = new ArrayList<Integer>();
boolean newUpdates = false;
if (c != null) {
try {
if (c.moveToFirst()) {
do {
updatedIds.add(c.getInt(UPDATE_DELETE_SERVER_ID_COLUMN));
newUpdates = true;
} while (c.moveToNext());
}
} finally {
c.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;
//}
// If the message is updated, make sure it's not deleted (we don't want to reload it)
if (newUpdates && updatedIds.contains(msg.mServerId)) {
Message currentMsg = Message.restoreMessageWithId(mContext, msg.mId);
if (currentMsg.mMailboxKey == mTrashMailboxId) {
userLog("PHEW! Didn't save deleted message with uid: " + msg.mServerId);
continue;
}
}
// Add the CPO's for this message
msg.addSaveOps(ops);
}
// Commit these messages
applyBatch(ops);
}
@ -1034,7 +1052,7 @@ public class Imap2SyncService extends AbstractSyncService {
values.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
// Save the attachments...
for (Attachment att: attachments) {
att.mAccountKey = mAccount.mId;
att.mAccountKey = mAccountId;
att.mMessageKey = msg.mId;
att.save(mContext);
}
@ -1166,7 +1184,7 @@ public class Imap2SyncService extends AbstractSyncService {
}
Mailbox m = new Mailbox();
m.mDisplayName = displayName;
m.mAccountKey = mAccount.mId;
m.mAccountKey = mAccountId;
m.mServerId = serverId;
if (parentName != null && !parentList.contains(parentName)) {
parentList.add(parentName);
@ -1184,7 +1202,7 @@ public class Imap2SyncService extends AbstractSyncService {
// TODO: Use narrower projection
Cursor c = mResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
Mailbox.ACCOUNT_KEY + "=?", new String[] {Long.toString(mAccount.mId)},
Mailbox.ACCOUNT_KEY + "=?", new String[] {Long.toString(mAccountId)},
MailboxColumns.SERVER_ID + " asc");
if (c == null) return;
int cnt = c.getCount();
@ -1307,7 +1325,7 @@ public class Imap2SyncService extends AbstractSyncService {
applyBatch(ops);
// Fixup parent stuff, flags...
MailboxUtilities.fixupUninitializedParentKeys(mContext,
Mailbox.ACCOUNT_KEY + "=" + mAccount.mId);
Mailbox.ACCOUNT_KEY + "=" + mAccountId);
} finally {
SyncManager.kick("folder list");
}
@ -1353,7 +1371,7 @@ public class Imap2SyncService extends AbstractSyncService {
}
}
private void processServerUpdates(ArrayList<Integer> deleteList, ContentValues values) {
private void processIntegers(ArrayList<Integer> deleteList, ContentValues values) {
int cnt = deleteList.size();
if (cnt > 0) {
ArrayList<ContentProviderOperation> ops =
@ -1473,9 +1491,9 @@ public class Imap2SyncService extends AbstractSyncService {
Reconciled r = reconcile(flag, deviceList, serverList);
ContentValues values = new ContentValues();
values.put(column, sense);
processServerUpdates(r.delete, values);
processIntegers(r.delete, values);
values.put(column, !sense);
processServerUpdates(r.insert, values);
processIntegers(r.insert, values);
}
}
@ -1659,88 +1677,139 @@ public class Imap2SyncService extends AbstractSyncService {
}
}
// Upload sent messages to server
private void doUpload(long messageId, String mailboxServerId) throws IOException,
MessagingException {
ContentValues values = new ContentValues();
CountingOutputStream out = new CountingOutputStream();
EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
Rfc822Output.writeTo(mContext,
messageId,
eolOut,
false /* do not use smart reply */,
false /* do not send BCC */);
eolOut.flush();
long len = out.getCount();
try {
String tag = writeCommand(mWriter, "append \"" +
encodeFolderName(mailboxServerId) +
"\" (\\seen) {" + len + '}');
String line = mReader.readLine();
if (line.startsWith("+")) {
userLog("append response: " + line);
eolOut = new EOLConvertingOutputStream(mSocket.getOutputStream());
Rfc822Output.writeTo(mContext,
messageId,
eolOut,
false /* do not use smart reply */,
false /* do not send BCC */);
eolOut.flush();
mWriter.write("\r\n");
mWriter.flush();
if (readResponse(mConnection.reader, tag).equals(IMAP_OK)) {
int serverId = 0;
String lc = mImapSuccessLine.toLowerCase();
int appendUid = lc.indexOf("appenduid");
if (appendUid > 0) {
Parser p = new Parser(lc, appendUid + 11);
// UIDVALIDITY (we don't need it)
p.parseInteger();
serverId = p.parseInteger();
}
values.put(SyncColumns.SERVER_ID, serverId);
mResolver.update(ContentUris.withAppendedId(Message.CONTENT_URI,
messageId), values, null, null);
} else {
userLog("Append failed: " + mImapErrorLine);
}
} else {
userLog("Append failed: " + line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 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 processUploads() {
Mailbox sentMailbox = Mailbox.restoreMailboxOfType(mContext, mAccountId, Mailbox.TYPE_SENT);
if (sentMailbox == null) {
// Nothing to do this time around; we'll check each time through the sync loop
return;
}
Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
MessageColumns.MAILBOX_KEY + "=? AND " + SyncColumns.SERVER_ID + " is null",
new String[] {Long.toString(sentMailbox.mId)}, null);
if (c != null) {
String sentMailboxServerId = sentMailbox.mServerId;
try {
// Upload these messages
while (c.moveToNext()) {
try {
doUpload(c.getLong(Message.ID_COLUMNS_ID_COLUMN), sentMailboxServerId);
} catch (IOException e) {
e.printStackTrace();
} catch (MessagingException e) {
e.printStackTrace();
}
}
} finally {
c.close();
}
}
}
private int[] getServerIds(String since) throws IOException {
String tag = writeCommand(mWriter, "uid search undeleted since " + since);
if (!readResponse(mReader, tag, "SEARCH").equals(IMAP_OK)) {
userLog("$$$ WHOA! Search failed? ");
return null;
}
userLog(">>> SEARCH RESULT");
String msgs;
Parser p;
if (mImapResponse.isEmpty()) {
return new int[0];
} else {
msgs = mImapResponse.get(0);
// Length of "* search"
p = new Parser(msgs, 8);
return p.gatherInts();
}
}
static private final int[] AUTO_WINDOW_VALUES = new int[] {
SyncWindow.SYNC_WINDOW_ALL, SyncWindow.SYNC_WINDOW_1_MONTH, SyncWindow.SYNC_WINDOW_2_WEEKS,
SyncWindow.SYNC_WINDOW_1_WEEK, SyncWindow.SYNC_WINDOW_3_DAYS};
/**
* Determine a sync window for this mailbox by trying different possibilities from among the
* allowed values (in AUTO_WINDOW_VALUES). We start testing with "all" unless there are more
* than AUTOMATIC_SYNC_WINDOW_LARGE_MAILBOX messages (we really don't want to load that many);
* otherwise, we start with one month. We'll pick any value that has fewer than
* AUTOMATIC_SYNC_WINDOW_MAX_MESSAGES messages (arbitrary, but reasonable)
* @return a reasonable sync window for this mailbox
* @throws IOException
*/
private int getAutoSyncWindow() throws IOException {
int i = (mLastExists > AUTOMATIC_SYNC_WINDOW_LARGE_MAILBOX) ? 1 : 0;
for (; i < AUTO_WINDOW_VALUES.length; i++) {
int window = AUTO_WINDOW_VALUES[i];
long days = SyncWindow.toDays(window);
Date date = new Date(System.currentTimeMillis() - (days*DAYS));
String since = IMAP_DATE_FORMAT.format(date);
int msgCount = getServerIds(since).length;
if (msgCount < AUTOMATIC_SYNC_WINDOW_MAX_MESSAGES) {
return window;
}
}
return SyncWindow.SYNC_WINDOW_1_DAY;
}
/**
* Process our list of requested attachment loads
* @throws IOException
*/
private void processRequests() throws IOException {
while (!mRequestQueue.isEmpty()) {
Request req = mRequestQueue.peek();
@ -1792,33 +1861,39 @@ public class Imap2SyncService extends AbstractSyncService {
// 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? ");
// We'll use 14 days as the "default"
long days = 14;
int lookback = mMailbox.mSyncLookback;
if (mMailbox.mType == Mailbox.TYPE_INBOX) {
lookback = mAccount.mSyncLookback;
}
if (lookback == SyncWindow.SYNC_WINDOW_AUTO) {
if (mLastExists >= 0) {
ContentValues values = new ContentValues();
lookback = getAutoSyncWindow();
Uri uri;
if (mMailbox.mType == Mailbox.TYPE_INBOX) {
values.put(AccountColumns.SYNC_LOOKBACK, lookback);
uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId);
} else {
values.put(MailboxColumns.SYNC_LOOKBACK, lookback);
uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId);
}
mResolver.update(uri, values, null, null);
}
}
if (lookback != SyncWindow.SYNC_WINDOW_UNKNOWN) {
days = SyncWindow.toDays(lookback);
}
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();
Date date = new Date(System.currentTimeMillis() - (days*DAYS));
String since = IMAP_DATE_FORMAT.format(date);
String tag;
int[] serverList = getServerIds(since);
if (serverList == null) {
// Do backoff; hope it works next time. Should never happen
mExitStatus = EXIT_IO_ERROR;
return;
}
Arrays.sort(serverList);
@ -1831,7 +1906,7 @@ public class Imap2SyncService extends AbstractSyncService {
deviceList = null;
int cnt = loadList.size();
// We load message headers 20 at a time at this point...
// We load message headers in batches
int idx= 1;
boolean loadedSome = false;
while (idx <= cnt) {
@ -1875,6 +1950,8 @@ public class Imap2SyncService extends AbstractSyncService {
reconcileState(getFlaggedUidList(), since, "FLAGGED", "flagged",
MessageColumns.FLAG_FAVORITE, false);
processUploads();
// We're done if not pushing...
if (mMailbox.mSyncInterval != Mailbox.CHECK_INTERVAL_PUSH) {
mExitStatus = EXIT_DONE;
@ -1899,97 +1976,151 @@ public class Imap2SyncService extends AbstractSyncService {
}
}
private void sendMail() {
long sentMailboxId = Mailbox.findMailboxOfType(mContext, mAccountId, Mailbox.TYPE_SENT);
if (sentMailboxId == Mailbox.NO_MAILBOX) {
// The user must choose a sent mailbox
mResolver.update(
ContentUris.withAppendedId(EmailContent.PICK_SENT_FOLDER_URI, mAccountId),
new ContentValues(), null, null);
}
Account account = Account.restoreAccountWithId(mContext, mAccountId);
if (account == null) {
return;
}
TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account));
// 1. Loop through all messages in the account's outbox
long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
if (outboxId == Mailbox.NO_MAILBOX) {
return;
}
Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) }, null);
ContentValues values = new ContentValues();
values.put(MessageColumns.MAILBOX_KEY, sentMailboxId);
try {
// 2. exit early
if (c.getCount() <= 0) {
return;
}
SmtpSender sender = new SmtpSender(mContext, account, mUserLog);
// 3. loop through the available messages and send them
while (c.moveToNext()) {
long messageId = -1;
try {
messageId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
// Don't send messages with unloaded attachments
if (Utility.hasUnloadedAttachments(mContext, messageId)) {
userLog("Can't send #" + messageId + "; unloaded attachments");
continue;
}
sender.sendMessage(messageId);
// Move to sent folder
mResolver.update(ContentUris.withAppendedId(Message.CONTENT_URI, messageId),
values, null, null);
} catch (MessagingException me) {
continue;
}
}
} finally {
c.close();
}
}
@Override
public void run() {
try {
// If we've been stopped, we're done
if (mStop) return;
// Check for Outbox (special "sync") and stopped
if (mMailbox.mType == Mailbox.TYPE_OUTBOX) {
sendMail();
mExitStatus = EXIT_DONE;
return;
} else 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);
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;
// 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;
}
} else {
userLog("Stopped sync finished.");
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;
}
// 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");
Imap2SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
} catch (RemoteException e1) {
// Don't care if this fails
}
} catch (ProviderUnavailableException e) {
Log.e(TAG, "EmailProvider unavailable; sync ended prematurely");
// Make sure ExchangeService knows about this
Imap2SyncManager.kick("sync finished");
}
}

View File

@ -0,0 +1,368 @@
/*
* Copyright (C) 2008 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.smtp;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Transport;
import com.android.emailcommon.utility.SSLUtils;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import javax.net.ssl.HostnameVerifier;
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;
/**
* This class implements the common aspects of "transport", one layer below the
* specific wire protocols such as POP3, IMAP, or SMTP.
*/
public class MailTransport implements Transport {
// TODO protected eventually
/*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
/*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
private static final HostnameVerifier HOSTNAME_VERIFIER =
HttpsURLConnection.getDefaultHostnameVerifier();
private String mHost;
private int mPort;
private String[] mUserInfoParts;
/**
* One of the {@code Transport.CONNECTION_SECURITY_*} values.
*/
private int mConnectionSecurity;
/**
* Whether or not to trust all server certificates (i.e. skip host verification) in SSL
* handshakes
*/
private boolean mTrustCertificates;
private Socket mSocket;
private InputStream mIn;
private OutputStream mOut;
private boolean mLog = true; // STOPSHIP Don't ship with this set to true
/**
* Simple constructor for starting from scratch. Call setUri() and setSecurity() to
* complete the configuration.
* @param debugLabel Label used for Log.d calls
*/
public MailTransport(boolean log) {
super();
mLog = log;
}
/**
* Returns a new transport, using the current transport as a model. The new transport is
* configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)}
* and {@link #setHost(String)} were invoked), but not opened or connected in any way.
*/
@Override
public Transport clone() {
MailTransport newObject = new MailTransport(mLog);
newObject.mLog = mLog;
newObject.mHost = mHost;
newObject.mPort = mPort;
if (mUserInfoParts != null) {
newObject.mUserInfoParts = mUserInfoParts.clone();
}
newObject.mConnectionSecurity = mConnectionSecurity;
newObject.mTrustCertificates = mTrustCertificates;
return newObject;
}
@Override
public void setHost(String host) {
mHost = host;
}
@Override
public void setPort(int port) {
mPort = port;
}
@Override
public String getHost() {
return mHost;
}
@Override
public int getPort() {
return mPort;
}
@Override
public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
mConnectionSecurity = connectionSecurity;
mTrustCertificates = trustAllCertificates;
}
@Override
public int getSecurity() {
return mConnectionSecurity;
}
@Override
public boolean canTrySslSecurity() {
return mConnectionSecurity == Transport.CONNECTION_SECURITY_SSL;
}
@Override
public boolean canTryTlsSecurity() {
return mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS;
}
@Override
public boolean canTrustAllCertificates() {
return mTrustCertificates;
}
/**
* Attempts to open a connection using the Uri supplied for connection parameters. Will attempt
* an SSL connection if indicated.
*/
@Override
public void open() throws MessagingException, CertificateValidationException {
if (mLog) {
Log.d(Logging.LOG_TAG, "*** SMTP open " +
getHost() + ":" + String.valueOf(getPort()));
}
try {
SocketAddress socketAddress = new InetSocketAddress(getHost(), getPort());
if (canTrySslSecurity()) {
mSocket = SSLUtils.getSSLSocketFactory(canTrustAllCertificates()).createSocket();
} else {
mSocket = new Socket();
}
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
// After the socket connects to an SSL server, confirm that the hostname is as expected
if (canTrySslSecurity() && !canTrustAllCertificates()) {
verifyHostname(mSocket, getHost());
}
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
} catch (SSLException e) {
if (mLog) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
if (mLog) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
}
}
/**
* Attempts to reopen a TLS connection using the Uri supplied for connection parameters.
*
* NOTE: No explicit hostname verification is required here, because it's handled automatically
* by the call to createSocket().
*
* TODO should we explicitly close the old socket? This seems funky to abandon it.
*/
@Override
public void reopenTls() throws MessagingException {
try {
mSocket = SSLUtils.getSSLSocketFactory(canTrustAllCertificates())
.createSocket(mSocket, getHost(), getPort(), true);
mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
} catch (SSLException e) {
if (mLog) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
if (mLog) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
}
}
/**
* 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 (!HOSTNAME_VERIFIER.verify(hostname, session)) {
throw new SSLPeerUnverifiedException(
"Certificate hostname not useable for server: " + hostname);
}
}
/**
* Set the socket timeout.
* @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or
* {@code 0} for an infinite timeout.
*/
@Override
public void setSoTimeout(int timeoutMilliseconds) throws SocketException {
mSocket.setSoTimeout(timeoutMilliseconds);
}
@Override
public boolean isOpen() {
return (mIn != null && mOut != null &&
mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
}
/**
* Close the connection. MUST NOT return any exceptions - must be "best effort" and safe.
*/
@Override
public void close() {
try {
mIn.close();
} catch (Exception e) {
// May fail if the connection is already closed.
}
try {
mOut.close();
} catch (Exception e) {
// May fail if the connection is already closed.
}
try {
mSocket.close();
} catch (Exception e) {
// May fail if the connection is already closed.
}
mIn = null;
mOut = null;
mSocket = null;
}
@Override
public InputStream getInputStream() {
return mIn;
}
@Override
public OutputStream getOutputStream() {
return mOut;
}
/**
* Writes a single line to the server using \r\n termination.
*/
@Override
public void writeLine(String s, String sensitiveReplacement) throws IOException {
if (mLog) {
if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
Log.d(Logging.LOG_TAG, ">>> " + sensitiveReplacement);
} else {
Log.d(Logging.LOG_TAG, ">>> " + s);
}
}
OutputStream out = getOutputStream();
out.write(s.getBytes());
out.write('\r');
out.write('\n');
out.flush();
}
/**
* Reads a single line from the server, using either \r\n or \n as the delimiter. The
* delimiter char(s) are not included in the result.
*/
@Override
public String readLine() throws IOException {
StringBuffer sb = new StringBuffer();
InputStream in = getInputStream();
int d;
while ((d = in.read()) != -1) {
if (((char)d) == '\r') {
continue;
} else if (((char)d) == '\n') {
break;
} else {
sb.append((char)d);
}
}
if (d == -1 && mLog) {
Log.d(Logging.LOG_TAG, "End of stream reached while trying to read line.");
}
String ret = sb.toString();
if (mLog) {
Log.d(Logging.LOG_TAG, "<<< " + ret);
}
return ret;
}
@Override
public InetAddress getLocalAddress() {
if (isOpen()) {
return mSocket.getLocalAddress();
} else {
return null;
}
}
}

View File

@ -0,0 +1,325 @@
/*
* Copyright (C) 2008 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.smtp;
import android.content.Context;
import android.util.Base64;
import android.util.Log;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.Rfc822Output;
import com.android.emailcommon.mail.Address;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Transport;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.utility.EOLConvertingOutputStream;
import java.io.IOException;
import java.net.Inet6Address;
import java.net.InetAddress;
import javax.net.ssl.SSLException;
/**
* This class handles all of the protocol-level aspects of sending messages via SMTP.
* TODO Remove dependence upon URI; there's no reason why we need it here
*/
public class SmtpSender {
private static final int DEFAULT_SMTP_PORT = 587;
private static final int DEFAULT_SMTP_SSL_PORT = 465;
private final Context mContext;
private Transport mTransport;
private String mUsername;
private String mPassword;
private boolean mLog;
/**
* Creates a new sender for the given account.
*/
public SmtpSender(Context context, Account account, boolean log) {
mContext = context;
mLog = log;
HostAuth sendAuth = account.getOrCreateHostAuthSend(context);
// defaults, which can be changed by security modifiers
int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
int defaultPort = DEFAULT_SMTP_PORT;
// check for security flags and apply changes
if ((sendAuth.mFlags & HostAuth.FLAG_SSL) != 0) {
connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
defaultPort = DEFAULT_SMTP_SSL_PORT;
} else if ((sendAuth.mFlags & HostAuth.FLAG_TLS) != 0) {
connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
}
boolean trustCertificates = ((sendAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0);
int port = defaultPort;
if (sendAuth.mPort != HostAuth.PORT_UNKNOWN) {
port = sendAuth.mPort;
}
mTransport = new MailTransport(mLog);
mTransport.setHost(sendAuth.mAddress);
mTransport.setPort(port);
mTransport.setSecurity(connectionSecurity, trustCertificates);
String[] userInfoParts = sendAuth.getLogin();
if (userInfoParts != null) {
mUsername = userInfoParts[0];
mPassword = userInfoParts[1];
}
}
/**
* For testing only. Injects a different transport. The transport should already be set
* up and ready to use. Do not use for real code.
* @param testTransport The Transport to inject and use for all future communication.
*/
/* package */ void setTransport(Transport testTransport) {
mTransport = testTransport;
}
public void open() throws MessagingException {
try {
mTransport.open();
// Eat the banner
executeSimpleCommand(null);
String localHost = "localhost";
// Try to get local address in the proper format.
InetAddress localAddress = mTransport.getLocalAddress();
if (localAddress != null) {
// Address Literal formatted in accordance to RFC2821 Sec. 4.1.3
StringBuilder sb = new StringBuilder();
sb.append('[');
if (localAddress instanceof Inet6Address) {
sb.append("IPv6:");
}
sb.append(localAddress.getHostAddress());
sb.append(']');
localHost = sb.toString();
}
String result = executeSimpleCommand("EHLO " + localHost);
/*
* TODO may need to add code to fall back to HELO I switched it from
* using HELO on non STARTTLS connections because of AOL's mail
* server. It won't let you use AUTH without EHLO.
* We should really be paying more attention to the capabilities
* and only attempting auth if it's available, and warning the user
* if not.
*/
if (mTransport.canTryTlsSecurity()) {
if (result.contains("STARTTLS")) {
executeSimpleCommand("STARTTLS");
mTransport.reopenTls();
/*
* Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
* Exim.
*/
result = executeSimpleCommand("EHLO " + localHost);
} else {
if (mLog) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
}
}
/*
* result contains the results of the EHLO in concatenated form
*/
boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$");
boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$");
if (mUsername != null && mUsername.length() > 0 && mPassword != null
&& mPassword.length() > 0) {
if (authPlainSupported) {
saslAuthPlain(mUsername, mPassword);
}
else if (authLoginSupported) {
saslAuthLogin(mUsername, mPassword);
}
else {
if (mLog) {
Log.d(Logging.LOG_TAG, "No valid authentication mechanism found.");
}
throw new MessagingException(MessagingException.AUTH_REQUIRED);
}
}
} catch (SSLException e) {
if (mLog) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
if (mLog) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
}
}
public void sendMessage(long messageId) throws MessagingException {
close();
open();
Message message = Message.restoreMessageWithId(mContext, messageId);
if (message == null) {
throw new MessagingException("Trying to send non-existent message id="
+ Long.toString(messageId));
}
Address from = Address.unpackFirst(message.mFrom);
Address[] to = Address.unpack(message.mTo);
Address[] cc = Address.unpack(message.mCc);
Address[] bcc = Address.unpack(message.mBcc);
try {
executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">");
for (Address address : to) {
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
}
for (Address address : cc) {
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
}
for (Address address : bcc) {
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
}
executeSimpleCommand("DATA");
// TODO byte stuffing
Rfc822Output.writeTo(mContext, messageId,
new EOLConvertingOutputStream(mTransport.getOutputStream()),
false /* do not use smart reply */,
false /* do not send BCC */);
executeSimpleCommand("\r\n.");
} catch (IOException ioe) {
throw new MessagingException("Unable to send message", ioe);
}
}
/**
* Close the protocol (and the transport below it).
*
* MUST NOT return any exceptions.
*/
public void close() {
mTransport.close();
}
/**
* Send a single command and wait for a single response. Handles responses that continue
* onto multiple lines. Throws MessagingException if response code is 4xx or 5xx. All traffic
* is logged (if debug logging is enabled) so do not use this function for user ID or password.
*
* @param command The command string to send to the server.
* @return Returns the response string from the server.
*/
private String executeSimpleCommand(String command) throws IOException, MessagingException {
return executeSensitiveCommand(command, null);
}
/**
* Send a single command and wait for a single response. Handles responses that continue
* onto multiple lines. Throws MessagingException if response code is 4xx or 5xx.
*
* @param command The command string to send to the server.
* @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
* please pass a replacement string here (for logging).
* @return Returns the response string from the server.
*/
private String executeSensitiveCommand(String command, String sensitiveReplacement)
throws IOException, MessagingException {
if (command != null) {
mTransport.writeLine(command, sensitiveReplacement);
}
String line = mTransport.readLine();
String result = line;
while (line.length() >= 4 && line.charAt(3) == '-') {
line = mTransport.readLine();
result += line.substring(3);
}
if (result.length() > 0) {
char c = result.charAt(0);
if ((c == '4') || (c == '5')) {
throw new MessagingException(result);
}
}
return result;
}
// C: AUTH LOGIN
// S: 334 VXNlcm5hbWU6
// C: d2VsZG9u
// S: 334 UGFzc3dvcmQ6
// C: dzNsZDBu
// S: 235 2.0.0 OK Authenticated
//
// Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads:
//
//
// C: AUTH LOGIN
// S: 334 Username:
// C: weldon
// S: 334 Password:
// C: w3ld0n
// S: 235 2.0.0 OK Authenticated
private void saslAuthLogin(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
try {
executeSimpleCommand("AUTH LOGIN");
executeSensitiveCommand(
Base64.encodeToString(username.getBytes(), Base64.NO_WRAP),
"/username redacted/");
executeSensitiveCommand(
Base64.encodeToString(password.getBytes(), Base64.NO_WRAP),
"/password redacted/");
}
catch (MessagingException me) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
throw new AuthenticationFailedException(me.getMessage());
}
throw me;
}
}
private void saslAuthPlain(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
byte[] data = ("\000" + username + "\000" + password).getBytes();
data = Base64.encode(data, Base64.NO_WRAP);
try {
executeSensitiveCommand("AUTH PLAIN " + new String(data), "AUTH PLAIN /redacted/");
}
catch (MessagingException me) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
throw new AuthenticationFailedException(me.getMessage());
}
throw me;
}
}
}

View File

@ -1296,7 +1296,7 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
roaming.</string>
<!-- This is shown when a user responds to a meeting invitation [CHAR LIMIT=none]-->
<string name="confirm_response">Sending response...</string>
<string name="confirm_response">Sending response&#8230;</string>
<!-- Displayed in the middle of the screen when the inbox is empty [CHAR LIMIT 100]-->
<string name="no_conversations">No messages.</string>
@ -1305,7 +1305,10 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
<string name="imap2_name">Push IMAP</string>
<string name="folder_picker_title">Picky, picky, picky!</string>
<string name="trash_folder_selection_title">Select trash folder</string>
<!-- Displayed when the user must pick his server's trash folder from a list [CHAR LIMIT 30]-->
<string name="trash_folder_selection_title">Select server trash folder</string>
<!-- Displayed when the user must pick his server's sent items folder from a list [CHAR LIMIT 30]-->
<string name="sent_folder_selection_title">Select server sent items folder</string>
<string name="create_new_folder">Create folder</string>
</resources>

View File

@ -112,6 +112,7 @@
email:syncIntervals="@array/account_settings_check_frequency_values_push"
email:defaultSyncInterval="push"
email:offerLookback="true"
email:offerTls="true"
email:usesSmtp="true"
email:offerAttachmentPreload="true"

View File

@ -27,6 +27,7 @@ import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Transport;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;

View File

@ -19,7 +19,6 @@ package com.android.email.mail.store;
import android.text.TextUtils;
import android.util.Log;
import com.android.email.mail.Transport;
import com.android.email.mail.store.ImapStore.ImapException;
import com.android.email.mail.store.imap.ImapConstants;
import com.android.email.mail.store.imap.ImapList;
@ -33,6 +32,7 @@ import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Transport;
import java.io.IOException;
import java.util.ArrayList;

View File

@ -29,8 +29,6 @@ import com.android.email.mail.store.imap.ImapList;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapString;
import com.android.email.mail.store.imap.ImapUtility;
import com.android.email.mail.transport.CountingOutputStream;
import com.android.email.mail.transport.EOLConvertingOutputStream;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.BinaryTempFileBody;
@ -48,6 +46,8 @@ import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.utility.CountingOutputStream;
import com.android.emailcommon.utility.EOLConvertingOutputStream;
import com.android.emailcommon.utility.Utility;
import com.google.common.annotations.VisibleForTesting;

View File

@ -28,7 +28,6 @@ import com.android.email.LegacyConversions;
import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.store.imap.ImapConstants;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapString;
@ -41,6 +40,7 @@ import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Transport;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;

View File

@ -22,7 +22,6 @@ import android.util.Log;
import com.android.email.R;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.transport.MailTransport;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
@ -31,6 +30,7 @@ import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Transport;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;

View File

@ -16,11 +16,11 @@
package com.android.email.mail.transport;
import com.android.email.mail.Transport;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Transport;
import com.android.emailcommon.utility.SSLUtils;
import android.util.Log;

View File

@ -21,7 +21,6 @@ import android.util.Base64;
import android.util.Log;
import com.android.email.mail.Sender;
import com.android.email.mail.Transport;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.Rfc822Output;
@ -29,9 +28,11 @@ import com.android.emailcommon.mail.Address;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Transport;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.utility.EOLConvertingOutputStream;
import java.io.IOException;
import java.net.Inet6Address;
@ -45,6 +46,9 @@ import javax.net.ssl.SSLException;
*/
public class SmtpSender extends Sender {
private static final int DEFAULT_SMTP_PORT = 587;
private static final int DEFAULT_SMTP_SSL_PORT = 465;
private final Context mContext;
private Transport mTransport;
private String mUsername;
@ -68,12 +72,12 @@ public class SmtpSender extends Sender {
}
// defaults, which can be changed by security modifiers
int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
int defaultPort = 587;
int defaultPort = DEFAULT_SMTP_PORT;
// check for security flags and apply changes
if ((sendAuth.mFlags & HostAuth.FLAG_SSL) != 0) {
connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
defaultPort = 465;
defaultPort = DEFAULT_SMTP_SSL_PORT;
} else if ((sendAuth.mFlags & HostAuth.FLAG_TLS) != 0) {
connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
}

View File

@ -187,6 +187,7 @@ public class EmailProvider extends ContentProvider {
private static final int ACCOUNT_DEFAULT_ID = ACCOUNT_BASE + 5;
private static final int ACCOUNT_CHECK = ACCOUNT_BASE + 6;
private static final int ACCOUNT_PICK_TRASH_FOLDER = ACCOUNT_BASE + 7;
private static final int ACCOUNT_PICK_SENT_FOLDER = ACCOUNT_BASE + 8;
private static final int MAILBOX_BASE = 0x1000;
private static final int MAILBOX = MAILBOX_BASE;
@ -475,6 +476,7 @@ public class EmailProvider extends ContentProvider {
matcher.addURI(EmailContent.AUTHORITY, "uidefaultrecentfolders/#",
UI_DEFAULT_RECENT_FOLDERS);
matcher.addURI(EmailContent.AUTHORITY, "pickTrashFolder/#", ACCOUNT_PICK_TRASH_FOLDER);
matcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", ACCOUNT_PICK_SENT_FOLDER);
}
/**
@ -1656,6 +1658,8 @@ outer:
switch (match) {
case ACCOUNT_PICK_TRASH_FOLDER:
return pickTrashFolder(uri);
case ACCOUNT_PICK_SENT_FOLDER:
return pickSentFolder(uri);
case UI_FOLDER:
return uiUpdateFolder(uri, values);
case UI_RECENT_FOLDERS:
@ -3742,6 +3746,7 @@ outer:
public static final String PICKER_UI_ACCOUNT = "picker_ui_account";
public static final String PICKER_MAILBOX_TYPE = "picker_mailbox_type";
public static final String PICKER_MESSAGE_ID = "picker_message_id";
public static final String PICKER_HEADER_ID = "picker_header_id";
private int uiDeleteMessage(Uri uri) {
final Context context = getContext();
@ -3767,7 +3772,7 @@ outer:
return uiUpdateMessage(uri, values);
}
private int pickTrashFolder(Uri uri) {
private int pickFolder(Uri uri, int type, int headerId) {
Context context = getContext();
Long acctId = Long.parseLong(uri.getLastPathSegment());
// For push imap, for example, we want the user to select the trash mailbox
@ -3779,7 +3784,8 @@ outer:
new com.android.mail.providers.Account(ac);
Intent intent = new Intent(context, FolderPickerActivity.class);
intent.putExtra(PICKER_UI_ACCOUNT, uiAccount);
intent.putExtra(PICKER_MAILBOX_TYPE, Mailbox.TYPE_TRASH);
intent.putExtra(PICKER_MAILBOX_TYPE, type);
intent.putExtra(PICKER_HEADER_ID, headerId);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return 1;
@ -3790,6 +3796,14 @@ outer:
}
}
private int pickTrashFolder(Uri uri) {
return pickFolder(uri, Mailbox.TYPE_TRASH, R.string.trash_folder_selection_title);
}
private int pickSentFolder(Uri uri) {
return pickFolder(uri, Mailbox.TYPE_SENT, R.string.sent_folder_selection_title);
}
private Cursor uiUndo(String[] projection) {
// First see if we have any operations saved
// TODO: Make sure seq matches

View File

@ -38,7 +38,12 @@ public class FolderPickerActivity extends Activity implements FolderPickerCallba
i.getParcelableExtra(EmailProvider.PICKER_UI_ACCOUNT);
mAccountId = Long.parseLong(account.uri.getLastPathSegment());
mMailboxType = i.getIntExtra(EmailProvider.PICKER_MAILBOX_TYPE, -1);
new FolderSelectionDialog(this, account, this).show();
int headerId = i.getIntExtra(EmailProvider.PICKER_HEADER_ID, 0);
if (headerId == 0) {
finish();
return;
}
new FolderSelectionDialog(this, account, this, headerId).show();
}
@Override

View File

@ -46,12 +46,12 @@ public class FolderSelectionDialog implements OnClickListener, OnMultiChoiceClic
final private FolderPickerCallback mCallback;
public FolderSelectionDialog(final Context context, Account account,
FolderPickerCallback callback) {
FolderPickerCallback callback, int headerId) {
mCallback = callback;
// Mapping of a folder's uri to its checked state
mCheckedState = new HashMap<Folder, Boolean>();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.trash_folder_selection_title);
builder.setTitle(headerId);
builder.setPositiveButton(R.string.ok, this);
builder.setNegativeButton(R.string.create_new_folder, this);
final Cursor foldersCursor = context.getContentResolver().query(