/* * 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.email.mail.store; import android.content.Context; import android.os.Bundle; import com.android.email.DebugUtils; import com.android.email.mail.Store; import com.android.email.mail.transport.MailTransport; import com.android.emailcommon.Logging; import com.android.emailcommon.internet.MimeMessage; 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.Folder.OpenMode; import com.android.emailcommon.mail.Message; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.SearchParams; import com.android.emailcommon.utility.LoggingInputStream; import com.android.emailcommon.utility.Utility; import com.android.mail.utils.LogUtils; import com.google.common.annotations.VisibleForTesting; import org.apache.james.mime4j.EOLConvertingInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; public class Pop3Store extends Store { // All flags defining debug or development code settings must be FALSE // when code is checked in or released. private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false; private static boolean DEBUG_LOG_RAW_STREAM = false; private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; /** The name of the only mailbox available to POP3 accounts */ private static final String POP3_MAILBOX_NAME = "INBOX"; private final HashMap mFolders = new HashMap(); private final Message[] mOneMessage = new Message[1]; /** * Static named constructor. */ public static Store newInstance(Account account, Context context) throws MessagingException { return new Pop3Store(context, account); } /** * Creates a new store for the given account. */ private Pop3Store(Context context, Account account) throws MessagingException { mContext = context; mAccount = account; HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); mTransport = new MailTransport(context, "POP3", recvAuth); String[] userInfoParts = recvAuth.getLogin(); 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(MailTransport testTransport) { mTransport = testTransport; } @Override public Folder getFolder(String name) { Folder folder = mFolders.get(name); if (folder == null) { folder = new Pop3Folder(name); mFolders.put(folder.getName(), folder); } return folder; } @Override public Folder[] updateFolders() { Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX); if (mailbox == null) { mailbox = Mailbox.newSystemMailbox(mContext, mAccount.mId, Mailbox.TYPE_INBOX); } if (mailbox.isSaved()) { mailbox.update(mContext, mailbox.toContentValues()); } else { mailbox.save(mContext); } return new Folder[] { getFolder(mailbox.mServerId) }; } /** * Used by account setup to test if an account's settings are appropriate. The definition * of "checked" here is simply, can you log into the account and does it meet some minimum set * of feature requirements? * * @throws MessagingException if there was some problem with the account */ @Override public Bundle checkSettings() throws MessagingException { Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME); Bundle bundle = null; // Close any open or half-open connections - checkSettings should always be "fresh" if (mTransport.isOpen()) { folder.close(false); } try { folder.open(OpenMode.READ_WRITE); bundle = folder.checkSettings(); } finally { folder.close(false); // false == don't expunge anything } return bundle; } public class Pop3Folder extends Folder { private final HashMap mUidToMsgMap = new HashMap(); private final HashMap mMsgNumToMsgMap = new HashMap(); private final HashMap mUidToMsgNumMap = new HashMap(); private final String mName; private int mMessageCount; private Pop3Capabilities mCapabilities; public Pop3Folder(String name) { if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) { mName = POP3_MAILBOX_NAME; } else { mName = name; } } /** * Used by account setup to test if an account's settings are appropriate. Here, we run * an additional test to see if UIDL is supported on the server. If it's not we * can't service this account. * * @return Bundle containing validation data (code and, if appropriate, error message) * @throws MessagingException if the account is not going to be useable */ public Bundle checkSettings() throws MessagingException { Bundle bundle = new Bundle(); int result = MessagingException.NO_ERROR; try { UidlParser parser = new UidlParser(); executeSimpleCommand("UIDL"); // drain the entire output, so additional communications don't get confused. String response; while ((response = mTransport.readLine(false)) != null) { parser.parseMultiLine(response); if (parser.mEndOfMessage) { break; } } } catch (IOException ioe) { mTransport.close(); result = MessagingException.IOERROR; bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage()); } bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); return bundle; } @Override public synchronized void open(OpenMode mode) throws MessagingException { if (mTransport.isOpen()) { return; } if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) { throw new MessagingException("Folder does not exist"); } try { mTransport.open(); // Eat the banner executeSimpleCommand(null); mCapabilities = getCapabilities(); if (mTransport.canTryTlsSecurity()) { if (mCapabilities.stls) { executeSimpleCommand("STLS"); mTransport.reopenTls(); } else { if (DebugUtils.DEBUG) { LogUtils.d(Logging.LOG_TAG, "TLS not supported but required"); } throw new MessagingException(MessagingException.TLS_REQUIRED); } } try { executeSensitiveCommand("USER " + mUsername, "USER /redacted/"); executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/"); } catch (MessagingException me) { if (DebugUtils.DEBUG) { LogUtils.d(Logging.LOG_TAG, me.toString()); } throw new AuthenticationFailedException(null, me); } } catch (IOException ioe) { mTransport.close(); if (DebugUtils.DEBUG) { LogUtils.d(Logging.LOG_TAG, ioe.toString()); } throw new MessagingException(MessagingException.IOERROR, ioe.toString()); } Exception statException = null; try { String response = executeSimpleCommand("STAT"); String[] parts = response.split(" "); if (parts.length < 2) { statException = new IOException(); } else { mMessageCount = Integer.parseInt(parts[1]); } } catch (MessagingException me) { statException = me; } catch (IOException ioe) { statException = ioe; } catch (NumberFormatException nfe) { statException = nfe; } if (statException != null) { mTransport.close(); if (DebugUtils.DEBUG) { LogUtils.d(Logging.LOG_TAG, statException.toString()); } throw new MessagingException("POP3 STAT", statException); } mUidToMsgMap.clear(); mMsgNumToMsgMap.clear(); mUidToMsgNumMap.clear(); } @Override public OpenMode getMode() { return OpenMode.READ_WRITE; } /** * Close the folder (and the transport below it). * * MUST NOT return any exceptions. * * @param expunge If true all deleted messages will be expunged (TODO - not implemented) */ @Override public void close(boolean expunge) { try { executeSimpleCommand("QUIT"); } catch (Exception e) { // ignore any problems here - just continue closing } mTransport.close(); } @Override public String getName() { return mName; } // POP3 does not folder creation @Override public boolean canCreate(FolderType type) { return false; } @Override public boolean create(FolderType type) { return false; } @Override public boolean exists() { return mName.equalsIgnoreCase(POP3_MAILBOX_NAME); } @Override public int getMessageCount() { return mMessageCount; } @Override public int getUnreadMessageCount() { return -1; } @Override public Message getMessage(String uid) throws MessagingException { if (mUidToMsgNumMap.size() == 0) { try { indexMsgNums(1, mMessageCount); } catch (IOException ioe) { mTransport.close(); if (DebugUtils.DEBUG) { LogUtils.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe); } throw new MessagingException("getMessages", ioe); } } Pop3Message message = mUidToMsgMap.get(uid); return message; } @Override public Pop3Message[] getMessages(int start, int end, MessageRetrievalListener listener) throws MessagingException { return null; } @Override public Pop3Message[] getMessages(long startDate, long endDate, MessageRetrievalListener listener) throws MessagingException { return null; } public Pop3Message[] getMessages(int end, final int limit) throws MessagingException { try { indexMsgNums(1, end); } catch (IOException ioe) { mTransport.close(); if (DebugUtils.DEBUG) { LogUtils.d(Logging.LOG_TAG, ioe.toString()); } throw new MessagingException("getMessages", ioe); } ArrayList messages = new ArrayList(); for (int msgNum = end; msgNum > 0 && (messages.size() < limit); msgNum--) { Pop3Message message = mMsgNumToMsgMap.get(msgNum); if (message != null) { messages.add(message); } } return messages.toArray(new Pop3Message[messages.size()]); } /** * Ensures that the given message set (from start to end inclusive) * has been queried so that uids are available in the local cache. * @param start * @param end * @throws MessagingException * @throws IOException */ private void indexMsgNums(int start, int end) throws MessagingException, IOException { if (!mMsgNumToMsgMap.isEmpty()) { return; } UidlParser parser = new UidlParser(); if (DEBUG_FORCE_SINGLE_LINE_UIDL || (mMessageCount > 5000)) { /* * In extreme cases we'll do a UIDL command per message instead of a bulk * download. */ for (int msgNum = start; msgNum <= end; msgNum++) { Pop3Message message = mMsgNumToMsgMap.get(msgNum); if (message == null) { String response = executeSimpleCommand("UIDL " + msgNum); if (!parser.parseSingleLine(response)) { throw new IOException(); } message = new Pop3Message(parser.mUniqueId, this); indexMessage(msgNum, message); } } } else { String response = executeSimpleCommand("UIDL"); while ((response = mTransport.readLine(false)) != null) { if (!parser.parseMultiLine(response)) { throw new IOException(); } if (parser.mEndOfMessage) { break; } int msgNum = parser.mMessageNumber; if (msgNum >= start && msgNum <= end) { Pop3Message message = mMsgNumToMsgMap.get(msgNum); if (message == null) { message = new Pop3Message(parser.mUniqueId, this); indexMessage(msgNum, message); } } } } } /** * Simple parser class for UIDL messages. * *

NOTE: In variance with RFC 1939, we allow multiple whitespace between the * message-number and unique-id fields. This provides greater compatibility with some * non-compliant POP3 servers, e.g. mail.comcast.net. */ /* package */ class UidlParser { /** * Caller can read back message-number from this field */ public int mMessageNumber; /** * Caller can read back unique-id from this field */ public String mUniqueId; /** * True if the response was "end-of-message" */ public boolean mEndOfMessage; /** * True if an error was reported */ public boolean mErr; /** * Construct & Initialize */ public UidlParser() { mErr = true; } /** * Parse a single-line response. This is returned from a command of the form * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or * "-ERR diagnostic text" * * @param response The string returned from the server * @return true if the string parsed as expected (e.g. no syntax problems) */ public boolean parseSingleLine(String response) { mErr = false; if (response == null || response.length() == 0) { return false; } char first = response.charAt(0); if (first == '+') { String[] uidParts = response.split(" +"); if (uidParts.length >= 3) { try { mMessageNumber = Integer.parseInt(uidParts[1]); } catch (NumberFormatException nfe) { return false; } mUniqueId = uidParts[2]; mEndOfMessage = true; return true; } } else if (first == '-') { mErr = true; return true; } return false; } /** * Parse a multi-line response. This is returned from a command of the form * "UIDL" and will be formatted as: "." or "msg-num unique-id". * * @param response The string returned from the server * @return true if the string parsed as expected (e.g. no syntax problems) */ public boolean parseMultiLine(String response) { mErr = false; if (response == null || response.length() == 0) { return false; } char first = response.charAt(0); if (first == '.') { mEndOfMessage = true; return true; } else { String[] uidParts = response.split(" +"); if (uidParts.length >= 2) { try { mMessageNumber = Integer.parseInt(uidParts[0]); } catch (NumberFormatException nfe) { return false; } mUniqueId = uidParts[1]; mEndOfMessage = false; return true; } } return false; } } private void indexMessage(int msgNum, Pop3Message message) { mMsgNumToMsgMap.put(msgNum, message); mUidToMsgMap.put(message.getUid(), message); mUidToMsgNumMap.put(message.getUid(), msgNum); } @Override public Message[] getMessages(String[] uids, MessageRetrievalListener listener) { throw new UnsupportedOperationException( "Pop3Folder.getMessage(MessageRetrievalListener)"); } /** * Fetch the items contained in the FetchProfile into the given set of * Messages in as efficient a manner as possible. * @param messages * @param fp * @throws MessagingException */ @Override public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { throw new UnsupportedOperationException( "Pop3Folder.fetch(Message[], FetchProfile, MessageRetrievalListener)"); } /** * Fetches the body of the given message, limiting the stored data * to the specified number of lines. If lines is -1 the entire message * is fetched. This is implemented with RETR for lines = -1 or TOP * for any other value. If the server does not support TOP it is * emulated with RETR and extra lines are thrown away. * * @param message * @param lines * @param callback optional callback that reports progress of the fetch */ public void fetchBody(Pop3Message message, int lines, EOLConvertingInputStream.Callback callback) throws IOException, MessagingException { String response = null; int messageId = mUidToMsgNumMap.get(message.getUid()); if (lines == -1) { // Fetch entire message response = executeSimpleCommand(String.format(Locale.US, "RETR %d", messageId)); } else { // Fetch partial message. Try "TOP", and fall back to slower "RETR" if necessary try { response = executeSimpleCommand( String.format(Locale.US, "TOP %d %d", messageId, lines)); } catch (MessagingException me) { try { response = executeSimpleCommand( String.format(Locale.US, "RETR %d", messageId)); } catch (MessagingException e) { LogUtils.w(Logging.LOG_TAG, "Can't read message " + messageId); } } } if (response != null) { try { int ok = response.indexOf("OK"); if (ok > 0) { try { int start = ok + 3; if (start > response.length()) { // No length was supplied, this is a protocol error. LogUtils.e(Logging.LOG_TAG, "No body length supplied"); message.setSize(0); } else { int end = response.indexOf(" ", start); final String intString; if (end > 0) { intString = response.substring(start, end); } else { intString = response.substring(start); } message.setSize(Integer.parseInt(intString)); } } catch (NumberFormatException e) { // We tried } } InputStream in = mTransport.getInputStream(); if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) { in = new LoggingInputStream(in); } message.parse(new Pop3ResponseInputStream(in), callback); } catch (MessagingException me) { /* * If we're only downloading headers it's possible * we'll get a broken MIME message which we're not * real worried about. If we've downloaded the body * and can't parse it we need to let the user know. */ if (lines == -1) { throw me; } } } } @Override public Flag[] getPermanentFlags() { return PERMANENT_FLAGS; } @Override public void appendMessage(Context context, Message message, boolean noTimeout) { } @Override public void delete(boolean recurse) { } @Override public Message[] expunge() { return null; } public void deleteMessage(Message message) throws MessagingException { mOneMessage[0] = message; setFlags(mOneMessage, PERMANENT_FLAGS, true); } @Override public void setFlags(Message[] messages, Flag[] flags, boolean value) throws MessagingException { if (!value || !Utility.arrayContains(flags, Flag.DELETED)) { /* * The only flagging we support is setting the Deleted flag. */ return; } try { for (Message message : messages) { try { String uid = message.getUid(); int msgNum = mUidToMsgNumMap.get(uid); executeSimpleCommand(String.format(Locale.US, "DELE %s", msgNum)); // Remove from the maps mMsgNumToMsgMap.remove(msgNum); mUidToMsgNumMap.remove(uid); } catch (MessagingException e) { // A failed deletion isn't a problem } } } catch (IOException ioe) { mTransport.close(); if (DebugUtils.DEBUG) { LogUtils.d(Logging.LOG_TAG, ioe.toString()); } throw new MessagingException("setFlags()", ioe); } } @Override public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) { throw new UnsupportedOperationException("copyMessages is not supported in POP3"); } private Pop3Capabilities getCapabilities() throws IOException { Pop3Capabilities capabilities = new Pop3Capabilities(); try { String response = executeSimpleCommand("CAPA"); while ((response = mTransport.readLine(true)) != null) { if (response.equals(".")) { break; } else if (response.equalsIgnoreCase("STLS")){ capabilities.stls = true; } } } catch (MessagingException me) { /* * The server may not support the CAPA command, so we just eat this Exception * and allow the empty capabilities object to be returned. */ } return capabilities; } /** * Send a single command and wait for a single line response. Reopens the connection, * if it is closed. Leaves the connection open. * * @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 line response. Reopens the connection, * if it is closed. Leaves the connection open. * * @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 { open(OpenMode.READ_WRITE); if (command != null) { mTransport.writeLine(command, sensitiveReplacement); } String response = mTransport.readLine(true); if (response.length() > 1 && response.charAt(0) == '-') { throw new MessagingException(response); } return response; } @Override public boolean equals(Object o) { if (o instanceof Pop3Folder) { return ((Pop3Folder) o).mName.equals(mName); } return super.equals(o); } @Override @VisibleForTesting public boolean isOpen() { return mTransport.isOpen(); } @Override public Message createMessage(String uid) { return new Pop3Message(uid, this); } @Override public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) { return null; } } public static class Pop3Message extends MimeMessage { public Pop3Message(String uid, Pop3Folder folder) { mUid = uid; mFolder = folder; mSize = -1; } public void setSize(int size) { mSize = size; } @Override public void parse(InputStream in) throws IOException, MessagingException { super.parse(in); } @Override public void setFlag(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); } } /** * POP3 Capabilities as defined in RFC 2449. This is not a complete list of CAPA * responses - just those that we use in this client. */ class Pop3Capabilities { /** The STLS (start TLS) command is supported */ public boolean stls; @Override public String toString() { return String.format("STLS %b", stls); } } // TODO figure out what is special about this and merge it into MailTransport class Pop3ResponseInputStream extends InputStream { private final InputStream mIn; private boolean mStartOfLine = true; private boolean mFinished; public Pop3ResponseInputStream(InputStream in) { mIn = in; } @Override public int read() throws IOException { if (mFinished) { return -1; } int d = mIn.read(); if (mStartOfLine && d == '.') { d = mIn.read(); if (d == '\r') { mFinished = true; mIn.read(); return -1; } } mStartOfLine = (d == '\n'); return d; } } }