• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.email.mail.store;
18 
19 import android.content.Context;
20 import android.os.Bundle;
21 
22 import com.android.email.DebugUtils;
23 import com.android.email.mail.Store;
24 import com.android.email.mail.transport.MailTransport;
25 import com.android.emailcommon.Logging;
26 import com.android.emailcommon.internet.MimeMessage;
27 import com.android.emailcommon.mail.AuthenticationFailedException;
28 import com.android.emailcommon.mail.FetchProfile;
29 import com.android.emailcommon.mail.Flag;
30 import com.android.emailcommon.mail.Folder;
31 import com.android.emailcommon.mail.Folder.OpenMode;
32 import com.android.emailcommon.mail.Message;
33 import com.android.emailcommon.mail.MessagingException;
34 import com.android.emailcommon.provider.Account;
35 import com.android.emailcommon.provider.HostAuth;
36 import com.android.emailcommon.provider.Mailbox;
37 import com.android.emailcommon.service.EmailServiceProxy;
38 import com.android.emailcommon.service.SearchParams;
39 import com.android.emailcommon.utility.LoggingInputStream;
40 import com.android.emailcommon.utility.Utility;
41 import com.android.mail.utils.LogUtils;
42 import com.google.common.annotations.VisibleForTesting;
43 
44 import org.apache.james.mime4j.EOLConvertingInputStream;
45 
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.Locale;
51 
52 public class Pop3Store extends Store {
53     // All flags defining debug or development code settings must be FALSE
54     // when code is checked in or released.
55     private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
56     private static boolean DEBUG_LOG_RAW_STREAM = false;
57 
58     private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
59     /** The name of the only mailbox available to POP3 accounts */
60     private static final String POP3_MAILBOX_NAME = "INBOX";
61     private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
62     private final Message[] mOneMessage = new Message[1];
63 
64     /**
65      * Static named constructor.
66      */
newInstance(Account account, Context context)67     public static Store newInstance(Account account, Context context) throws MessagingException {
68         return new Pop3Store(context, account);
69     }
70 
71     /**
72      * Creates a new store for the given account.
73      */
Pop3Store(Context context, Account account)74     private Pop3Store(Context context, Account account) throws MessagingException {
75         mContext = context;
76         mAccount = account;
77 
78         HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
79         mTransport = new MailTransport(context, "POP3", recvAuth);
80         String[] userInfoParts = recvAuth.getLogin();
81         mUsername = userInfoParts[0];
82         mPassword = userInfoParts[1];
83     }
84 
85     /**
86      * For testing only.  Injects a different transport.  The transport should already be set
87      * up and ready to use.  Do not use for real code.
88      * @param testTransport The Transport to inject and use for all future communication.
89      */
setTransport(MailTransport testTransport)90     /* package */ void setTransport(MailTransport testTransport) {
91         mTransport = testTransport;
92     }
93 
94     @Override
getFolder(String name)95     public Folder getFolder(String name) {
96         Folder folder = mFolders.get(name);
97         if (folder == null) {
98             folder = new Pop3Folder(name);
99             mFolders.put(folder.getName(), folder);
100         }
101         return folder;
102     }
103 
104     @Override
updateFolders()105     public Folder[] updateFolders() {
106         Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
107         if (mailbox == null) {
108             mailbox = Mailbox.newSystemMailbox(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
109         }
110         if (mailbox.isSaved()) {
111             mailbox.update(mContext, mailbox.toContentValues());
112         } else {
113             mailbox.save(mContext);
114         }
115         return new Folder[] { getFolder(mailbox.mServerId) };
116     }
117 
118     /**
119      * Used by account setup to test if an account's settings are appropriate.  The definition
120      * of "checked" here is simply, can you log into the account and does it meet some minimum set
121      * of feature requirements?
122      *
123      * @throws MessagingException if there was some problem with the account
124      */
125     @Override
checkSettings()126     public Bundle checkSettings() throws MessagingException {
127         Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME);
128         Bundle bundle = null;
129         // Close any open or half-open connections - checkSettings should always be "fresh"
130         if (mTransport.isOpen()) {
131             folder.close(false);
132         }
133         try {
134             folder.open(OpenMode.READ_WRITE);
135             bundle = folder.checkSettings();
136         } finally {
137             folder.close(false);    // false == don't expunge anything
138         }
139         return bundle;
140     }
141 
142     public class Pop3Folder extends Folder {
143         private final HashMap<String, Pop3Message> mUidToMsgMap
144                 = new HashMap<String, Pop3Message>();
145         private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap
146                 = new HashMap<Integer, Pop3Message>();
147         private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
148         private final String mName;
149         private int mMessageCount;
150         private Pop3Capabilities mCapabilities;
151 
Pop3Folder(String name)152         public Pop3Folder(String name) {
153             if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
154                 mName = POP3_MAILBOX_NAME;
155             } else {
156                 mName = name;
157             }
158         }
159 
160         /**
161          * Used by account setup to test if an account's settings are appropriate.  Here, we run
162          * an additional test to see if UIDL is supported on the server. If it's not we
163          * can't service this account.
164          *
165          * @return Bundle containing validation data (code and, if appropriate, error message)
166          * @throws MessagingException if the account is not going to be useable
167          */
checkSettings()168         public Bundle checkSettings() throws MessagingException {
169             Bundle bundle = new Bundle();
170             int result = MessagingException.NO_ERROR;
171             try {
172                 UidlParser parser = new UidlParser();
173                 executeSimpleCommand("UIDL");
174                 // drain the entire output, so additional communications don't get confused.
175                 String response;
176                 while ((response = mTransport.readLine(false)) != null) {
177                     parser.parseMultiLine(response);
178                     if (parser.mEndOfMessage) {
179                         break;
180                     }
181                 }
182             } catch (IOException ioe) {
183                 mTransport.close();
184                 result = MessagingException.IOERROR;
185                 bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE,
186                         ioe.getMessage());
187             }
188             bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
189             return bundle;
190         }
191 
192         @Override
open(OpenMode mode)193         public synchronized void open(OpenMode mode) throws MessagingException {
194             if (mTransport.isOpen()) {
195                 return;
196             }
197 
198             if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
199                 throw new MessagingException("Folder does not exist");
200             }
201 
202             try {
203                 mTransport.open();
204 
205                 // Eat the banner
206                 executeSimpleCommand(null);
207 
208                 mCapabilities = getCapabilities();
209 
210                 if (mTransport.canTryTlsSecurity()) {
211                     if (mCapabilities.stls) {
212                         executeSimpleCommand("STLS");
213                         mTransport.reopenTls();
214                     } else {
215                         if (DebugUtils.DEBUG) {
216                             LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
217                         }
218                         throw new MessagingException(MessagingException.TLS_REQUIRED);
219                     }
220                 }
221 
222                 try {
223                     executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
224                     executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
225                 } catch (MessagingException me) {
226                     if (DebugUtils.DEBUG) {
227                         LogUtils.d(Logging.LOG_TAG, me.toString());
228                     }
229                     throw new AuthenticationFailedException(null, me);
230                 }
231             } catch (IOException ioe) {
232                 mTransport.close();
233                 if (DebugUtils.DEBUG) {
234                     LogUtils.d(Logging.LOG_TAG, ioe.toString());
235                 }
236                 throw new MessagingException(MessagingException.IOERROR, ioe.toString());
237             }
238 
239             Exception statException = null;
240             try {
241                 String response = executeSimpleCommand("STAT");
242                 String[] parts = response.split(" ");
243                 if (parts.length < 2) {
244                     statException = new IOException();
245                 } else {
246                     mMessageCount = Integer.parseInt(parts[1]);
247                 }
248             } catch (MessagingException me) {
249                 statException = me;
250             } catch (IOException ioe) {
251                 statException = ioe;
252             } catch (NumberFormatException nfe) {
253                 statException = nfe;
254             }
255             if (statException != null) {
256                 mTransport.close();
257                 if (DebugUtils.DEBUG) {
258                     LogUtils.d(Logging.LOG_TAG, statException.toString());
259                 }
260                 throw new MessagingException("POP3 STAT", statException);
261             }
262             mUidToMsgMap.clear();
263             mMsgNumToMsgMap.clear();
264             mUidToMsgNumMap.clear();
265         }
266 
267         @Override
getMode()268         public OpenMode getMode() {
269             return OpenMode.READ_WRITE;
270         }
271 
272         /**
273          * Close the folder (and the transport below it).
274          *
275          * MUST NOT return any exceptions.
276          *
277          * @param expunge If true all deleted messages will be expunged (TODO - not implemented)
278          */
279         @Override
close(boolean expunge)280         public void close(boolean expunge) {
281             try {
282                 executeSimpleCommand("QUIT");
283             }
284             catch (Exception e) {
285                 // ignore any problems here - just continue closing
286             }
287             mTransport.close();
288         }
289 
290         @Override
getName()291         public String getName() {
292             return mName;
293         }
294 
295         // POP3 does not folder creation
296         @Override
canCreate(FolderType type)297         public boolean canCreate(FolderType type) {
298             return false;
299         }
300 
301         @Override
create(FolderType type)302         public boolean create(FolderType type) {
303             return false;
304         }
305 
306         @Override
exists()307         public boolean exists() {
308             return mName.equalsIgnoreCase(POP3_MAILBOX_NAME);
309         }
310 
311         @Override
getMessageCount()312         public int getMessageCount() {
313             return mMessageCount;
314         }
315 
316         @Override
getUnreadMessageCount()317         public int getUnreadMessageCount() {
318             return -1;
319         }
320 
321         @Override
getMessage(String uid)322         public Message getMessage(String uid) throws MessagingException {
323             if (mUidToMsgNumMap.size() == 0) {
324                 try {
325                     indexMsgNums(1, mMessageCount);
326                 } catch (IOException ioe) {
327                     mTransport.close();
328                     if (DebugUtils.DEBUG) {
329                         LogUtils.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe);
330                     }
331                     throw new MessagingException("getMessages", ioe);
332                 }
333             }
334             Pop3Message message = mUidToMsgMap.get(uid);
335             return message;
336         }
337 
338         @Override
getMessages(int start, int end, MessageRetrievalListener listener)339         public Pop3Message[] getMessages(int start, int end, MessageRetrievalListener listener)
340                 throws MessagingException {
341             return null;
342         }
343 
344         @Override
getMessages(long startDate, long endDate, MessageRetrievalListener listener)345         public Pop3Message[] getMessages(long startDate, long endDate,
346                 MessageRetrievalListener listener) throws MessagingException {
347             return null;
348         }
349 
getMessages(int end, final int limit)350         public Pop3Message[] getMessages(int end, final int limit)
351                 throws MessagingException {
352             try {
353                 indexMsgNums(1, end);
354             } catch (IOException ioe) {
355                 mTransport.close();
356                 if (DebugUtils.DEBUG) {
357                     LogUtils.d(Logging.LOG_TAG, ioe.toString());
358                 }
359                 throw new MessagingException("getMessages", ioe);
360             }
361             ArrayList<Message> messages = new ArrayList<Message>();
362             for (int msgNum = end; msgNum > 0 && (messages.size() < limit); msgNum--) {
363                 Pop3Message message = mMsgNumToMsgMap.get(msgNum);
364                 if (message != null) {
365                     messages.add(message);
366                 }
367             }
368             return messages.toArray(new Pop3Message[messages.size()]);
369         }
370 
371         /**
372          * Ensures that the given message set (from start to end inclusive)
373          * has been queried so that uids are available in the local cache.
374          * @param start
375          * @param end
376          * @throws MessagingException
377          * @throws IOException
378          */
indexMsgNums(int start, int end)379         private void indexMsgNums(int start, int end)
380                 throws MessagingException, IOException {
381             if (!mMsgNumToMsgMap.isEmpty()) {
382                 return;
383             }
384             UidlParser parser = new UidlParser();
385             if (DEBUG_FORCE_SINGLE_LINE_UIDL || (mMessageCount > 5000)) {
386                 /*
387                  * In extreme cases we'll do a UIDL command per message instead of a bulk
388                  * download.
389                  */
390                 for (int msgNum = start; msgNum <= end; msgNum++) {
391                     Pop3Message message = mMsgNumToMsgMap.get(msgNum);
392                     if (message == null) {
393                         String response = executeSimpleCommand("UIDL " + msgNum);
394                         if (!parser.parseSingleLine(response)) {
395                             throw new IOException();
396                         }
397                         message = new Pop3Message(parser.mUniqueId, this);
398                         indexMessage(msgNum, message);
399                     }
400                 }
401             } else {
402                 String response = executeSimpleCommand("UIDL");
403                 while ((response = mTransport.readLine(false)) != null) {
404                     if (!parser.parseMultiLine(response)) {
405                         throw new IOException();
406                     }
407                     if (parser.mEndOfMessage) {
408                         break;
409                     }
410                     int msgNum = parser.mMessageNumber;
411                     if (msgNum >= start && msgNum <= end) {
412                         Pop3Message message = mMsgNumToMsgMap.get(msgNum);
413                         if (message == null) {
414                             message = new Pop3Message(parser.mUniqueId, this);
415                             indexMessage(msgNum, message);
416                         }
417                     }
418                 }
419             }
420         }
421 
422         /**
423          * Simple parser class for UIDL messages.
424          *
425          * <p>NOTE:  In variance with RFC 1939, we allow multiple whitespace between the
426          * message-number and unique-id fields.  This provides greater compatibility with some
427          * non-compliant POP3 servers, e.g. mail.comcast.net.
428          */
429         /* package */ class UidlParser {
430 
431             /**
432              * Caller can read back message-number from this field
433              */
434             public int mMessageNumber;
435             /**
436              * Caller can read back unique-id from this field
437              */
438             public String mUniqueId;
439             /**
440              * True if the response was "end-of-message"
441              */
442             public boolean mEndOfMessage;
443             /**
444              * True if an error was reported
445              */
446             public boolean mErr;
447 
448             /**
449              * Construct & Initialize
450              */
UidlParser()451             public UidlParser() {
452                 mErr = true;
453             }
454 
455             /**
456              * Parse a single-line response.  This is returned from a command of the form
457              * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or
458              * "-ERR diagnostic text"
459              *
460              * @param response The string returned from the server
461              * @return true if the string parsed as expected (e.g. no syntax problems)
462              */
parseSingleLine(String response)463             public boolean parseSingleLine(String response) {
464                 mErr = false;
465                 if (response == null || response.length() == 0) {
466                     return false;
467                 }
468                 char first = response.charAt(0);
469                 if (first == '+') {
470                     String[] uidParts = response.split(" +");
471                     if (uidParts.length >= 3) {
472                         try {
473                             mMessageNumber = Integer.parseInt(uidParts[1]);
474                         } catch (NumberFormatException nfe) {
475                             return false;
476                         }
477                         mUniqueId = uidParts[2];
478                         mEndOfMessage = true;
479                         return true;
480                     }
481                 } else if (first == '-') {
482                     mErr = true;
483                     return true;
484                 }
485                 return false;
486             }
487 
488             /**
489              * Parse a multi-line response.  This is returned from a command of the form
490              * "UIDL" and will be formatted as: "." or "msg-num unique-id".
491              *
492              * @param response The string returned from the server
493              * @return true if the string parsed as expected (e.g. no syntax problems)
494              */
parseMultiLine(String response)495             public boolean parseMultiLine(String response) {
496                 mErr = false;
497                 if (response == null || response.length() == 0) {
498                     return false;
499                 }
500                 char first = response.charAt(0);
501                 if (first == '.') {
502                     mEndOfMessage = true;
503                     return true;
504                 } else {
505                     String[] uidParts = response.split(" +");
506                     if (uidParts.length >= 2) {
507                         try {
508                             mMessageNumber = Integer.parseInt(uidParts[0]);
509                         } catch (NumberFormatException nfe) {
510                             return false;
511                         }
512                         mUniqueId = uidParts[1];
513                         mEndOfMessage = false;
514                         return true;
515                     }
516                 }
517                 return false;
518             }
519         }
520 
indexMessage(int msgNum, Pop3Message message)521         private void indexMessage(int msgNum, Pop3Message message) {
522             mMsgNumToMsgMap.put(msgNum, message);
523             mUidToMsgMap.put(message.getUid(), message);
524             mUidToMsgNumMap.put(message.getUid(), msgNum);
525         }
526 
527         @Override
getMessages(String[] uids, MessageRetrievalListener listener)528         public Message[] getMessages(String[] uids, MessageRetrievalListener listener) {
529             throw new UnsupportedOperationException(
530                     "Pop3Folder.getMessage(MessageRetrievalListener)");
531         }
532 
533         /**
534          * Fetch the items contained in the FetchProfile into the given set of
535          * Messages in as efficient a manner as possible.
536          * @param messages
537          * @param fp
538          * @throws MessagingException
539          */
540         @Override
fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)541         public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
542                 throws MessagingException {
543             throw new UnsupportedOperationException(
544                     "Pop3Folder.fetch(Message[], FetchProfile, MessageRetrievalListener)");
545         }
546 
547         /**
548          * Fetches the body of the given message, limiting the stored data
549          * to the specified number of lines. If lines is -1 the entire message
550          * is fetched. This is implemented with RETR for lines = -1 or TOP
551          * for any other value. If the server does not support TOP it is
552          * emulated with RETR and extra lines are thrown away.
553          *
554          * @param message
555          * @param lines
556          * @param callback optional callback that reports progress of the fetch
557          */
fetchBody(Pop3Message message, int lines, EOLConvertingInputStream.Callback callback)558         public void fetchBody(Pop3Message message, int lines,
559                 EOLConvertingInputStream.Callback callback) throws IOException, MessagingException {
560             String response = null;
561             int messageId = mUidToMsgNumMap.get(message.getUid());
562             if (lines == -1) {
563                 // Fetch entire message
564                 response = executeSimpleCommand(String.format(Locale.US, "RETR %d", messageId));
565             } else {
566                 // Fetch partial message.  Try "TOP", and fall back to slower "RETR" if necessary
567                 try {
568                     response = executeSimpleCommand(
569                             String.format(Locale.US, "TOP %d %d", messageId,  lines));
570                 } catch (MessagingException me) {
571                     try {
572                         response = executeSimpleCommand(
573                                 String.format(Locale.US, "RETR %d", messageId));
574                     } catch (MessagingException e) {
575                         LogUtils.w(Logging.LOG_TAG, "Can't read message " + messageId);
576                     }
577                 }
578             }
579             if (response != null)  {
580                 try {
581                     int ok = response.indexOf("OK");
582                     if (ok > 0) {
583                         try {
584                             int start = ok + 3;
585                             if (start > response.length()) {
586                                 // No length was supplied, this is a protocol error.
587                                 LogUtils.e(Logging.LOG_TAG, "No body length supplied");
588                                 message.setSize(0);
589                             } else {
590                                 int end = response.indexOf(" ", start);
591                                 final String intString;
592                                 if (end > 0) {
593                                     intString = response.substring(start, end);
594                                 } else {
595                                     intString = response.substring(start);
596                                 }
597                                 message.setSize(Integer.parseInt(intString));
598                             }
599                         } catch (NumberFormatException e) {
600                             // We tried
601                         }
602                     }
603                     InputStream in = mTransport.getInputStream();
604                     if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) {
605                         in = new LoggingInputStream(in);
606                     }
607                     message.parse(new Pop3ResponseInputStream(in), callback);
608                 }
609                 catch (MessagingException me) {
610                     /*
611                      * If we're only downloading headers it's possible
612                      * we'll get a broken MIME message which we're not
613                      * real worried about. If we've downloaded the body
614                      * and can't parse it we need to let the user know.
615                      */
616                     if (lines == -1) {
617                         throw me;
618                     }
619                 }
620             }
621         }
622 
623         @Override
getPermanentFlags()624         public Flag[] getPermanentFlags() {
625             return PERMANENT_FLAGS;
626         }
627 
628         @Override
appendMessage(Context context, Message message, boolean noTimeout)629         public void appendMessage(Context context, Message message, boolean noTimeout) {
630         }
631 
632         @Override
delete(boolean recurse)633         public void delete(boolean recurse) {
634         }
635 
636         @Override
expunge()637         public Message[] expunge() {
638             return null;
639         }
640 
deleteMessage(Message message)641         public void deleteMessage(Message message) throws MessagingException {
642             mOneMessage[0] = message;
643             setFlags(mOneMessage, PERMANENT_FLAGS, true);
644         }
645 
646         @Override
setFlags(Message[] messages, Flag[] flags, boolean value)647         public void setFlags(Message[] messages, Flag[] flags, boolean value)
648                 throws MessagingException {
649             if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
650                 /*
651                  * The only flagging we support is setting the Deleted flag.
652                  */
653                 return;
654             }
655             try {
656                 for (Message message : messages) {
657                     try {
658                         String uid = message.getUid();
659                         int msgNum = mUidToMsgNumMap.get(uid);
660                         executeSimpleCommand(String.format(Locale.US, "DELE %s", msgNum));
661                         // Remove from the maps
662                         mMsgNumToMsgMap.remove(msgNum);
663                         mUidToMsgNumMap.remove(uid);
664                     } catch (MessagingException e) {
665                         // A failed deletion isn't a problem
666                     }
667                 }
668             }
669             catch (IOException ioe) {
670                 mTransport.close();
671                 if (DebugUtils.DEBUG) {
672                     LogUtils.d(Logging.LOG_TAG, ioe.toString());
673                 }
674                 throw new MessagingException("setFlags()", ioe);
675             }
676         }
677 
678         @Override
copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks)679         public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) {
680             throw new UnsupportedOperationException("copyMessages is not supported in POP3");
681         }
682 
getCapabilities()683         private Pop3Capabilities getCapabilities() throws IOException {
684             Pop3Capabilities capabilities = new Pop3Capabilities();
685             try {
686                 String response = executeSimpleCommand("CAPA");
687                 while ((response = mTransport.readLine(true)) != null) {
688                     if (response.equals(".")) {
689                         break;
690                     } else if (response.equalsIgnoreCase("STLS")){
691                         capabilities.stls = true;
692                     }
693                 }
694             }
695             catch (MessagingException me) {
696                 /*
697                  * The server may not support the CAPA command, so we just eat this Exception
698                  * and allow the empty capabilities object to be returned.
699                  */
700             }
701             return capabilities;
702         }
703 
704         /**
705          * Send a single command and wait for a single line response.  Reopens the connection,
706          * if it is closed.  Leaves the connection open.
707          *
708          * @param command The command string to send to the server.
709          * @return Returns the response string from the server.
710          */
executeSimpleCommand(String command)711         private String executeSimpleCommand(String command) throws IOException, MessagingException {
712             return executeSensitiveCommand(command, null);
713         }
714 
715         /**
716          * Send a single command and wait for a single line response.  Reopens the connection,
717          * if it is closed.  Leaves the connection open.
718          *
719          * @param command The command string to send to the server.
720          * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
721          * please pass a replacement string here (for logging).
722          * @return Returns the response string from the server.
723          */
executeSensitiveCommand(String command, String sensitiveReplacement)724         private String executeSensitiveCommand(String command, String sensitiveReplacement)
725                 throws IOException, MessagingException {
726             open(OpenMode.READ_WRITE);
727 
728             if (command != null) {
729                 mTransport.writeLine(command, sensitiveReplacement);
730             }
731 
732             String response = mTransport.readLine(true);
733 
734             if (response.length() > 1 && response.charAt(0) == '-') {
735                 throw new MessagingException(response);
736             }
737 
738             return response;
739         }
740 
741         @Override
equals(Object o)742         public boolean equals(Object o) {
743             if (o instanceof Pop3Folder) {
744                 return ((Pop3Folder) o).mName.equals(mName);
745             }
746             return super.equals(o);
747         }
748 
749         @Override
750         @VisibleForTesting
isOpen()751         public boolean isOpen() {
752             return mTransport.isOpen();
753         }
754 
755         @Override
createMessage(String uid)756         public Message createMessage(String uid) {
757             return new Pop3Message(uid, this);
758         }
759 
760         @Override
getMessages(SearchParams params, MessageRetrievalListener listener)761         public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) {
762             return null;
763         }
764     }
765 
766     public static class Pop3Message extends MimeMessage {
Pop3Message(String uid, Pop3Folder folder)767         public Pop3Message(String uid, Pop3Folder folder) {
768             mUid = uid;
769             mFolder = folder;
770             mSize = -1;
771         }
772 
setSize(int size)773         public void setSize(int size) {
774             mSize = size;
775         }
776 
777         @Override
parse(InputStream in)778         public void parse(InputStream in) throws IOException, MessagingException {
779             super.parse(in);
780         }
781 
782         @Override
setFlag(Flag flag, boolean set)783         public void setFlag(Flag flag, boolean set) throws MessagingException {
784             super.setFlag(flag, set);
785             mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
786         }
787     }
788 
789     /**
790      * POP3 Capabilities as defined in RFC 2449.  This is not a complete list of CAPA
791      * responses - just those that we use in this client.
792      */
793     class Pop3Capabilities {
794         /** The STLS (start TLS) command is supported */
795         public boolean stls;
796 
797         @Override
toString()798         public String toString() {
799             return String.format("STLS %b", stls);
800         }
801     }
802 
803     // TODO figure out what is special about this and merge it into MailTransport
804     class Pop3ResponseInputStream extends InputStream {
805         private final InputStream mIn;
806         private boolean mStartOfLine = true;
807         private boolean mFinished;
808 
Pop3ResponseInputStream(InputStream in)809         public Pop3ResponseInputStream(InputStream in) {
810             mIn = in;
811         }
812 
813         @Override
read()814         public int read() throws IOException {
815             if (mFinished) {
816                 return -1;
817             }
818             int d = mIn.read();
819             if (mStartOfLine && d == '.') {
820                 d = mIn.read();
821                 if (d == '\r') {
822                     mFinished = true;
823                     mIn.read();
824                     return -1;
825                 }
826             }
827 
828             mStartOfLine = (d == '\n');
829 
830             return d;
831         }
832     }
833 }
834