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:
parent
c992071671
commit
a8b683cf3f
|
@ -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;
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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…</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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue