• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.text.TextUtils;
20 import android.util.Base64;
21 
22 import com.android.email.mail.internet.AuthenticationCache;
23 import com.android.email.mail.store.ImapStore.ImapException;
24 import com.android.email.mail.store.imap.ImapConstants;
25 import com.android.email.mail.store.imap.ImapList;
26 import com.android.email.mail.store.imap.ImapResponse;
27 import com.android.email.mail.store.imap.ImapResponseParser;
28 import com.android.email.mail.store.imap.ImapUtility;
29 import com.android.email.mail.transport.DiscourseLogger;
30 import com.android.email.mail.transport.MailTransport;
31 import com.android.email2.ui.MailActivityEmail;
32 import com.android.emailcommon.Logging;
33 import com.android.emailcommon.mail.AuthenticationFailedException;
34 import com.android.emailcommon.mail.CertificateValidationException;
35 import com.android.emailcommon.mail.MessagingException;
36 import com.android.mail.utils.LogUtils;
37 
38 import java.io.IOException;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.concurrent.atomic.AtomicInteger;
43 
44 import javax.net.ssl.SSLException;
45 
46 /**
47  * A cacheable class that stores the details for a single IMAP connection.
48  */
49 class ImapConnection {
50     // Always check in FALSE
51     private static final boolean DEBUG_FORCE_SEND_ID = false;
52 
53     /** ID capability per RFC 2971*/
54     public static final int CAPABILITY_ID        = 1 << 0;
55     /** NAMESPACE capability per RFC 2342 */
56     public static final int CAPABILITY_NAMESPACE = 1 << 1;
57     /** STARTTLS capability per RFC 3501 */
58     public static final int CAPABILITY_STARTTLS  = 1 << 2;
59     /** UIDPLUS capability per RFC 4315 */
60     public static final int CAPABILITY_UIDPLUS   = 1 << 3;
61 
62     /** The capabilities supported; a set of CAPABILITY_* values. */
63     private int mCapabilities;
64     static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
65     MailTransport mTransport;
66     private ImapResponseParser mParser;
67     private ImapStore mImapStore;
68     private String mLoginPhrase;
69     private String mAccessToken;
70     private String mIdPhrase = null;
71 
72     /** # of command/response lines to log upon crash. */
73     private static final int DISCOURSE_LOGGER_SIZE = 64;
74     private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
75     /**
76      * Next tag to use.  All connections associated to the same ImapStore instance share the same
77      * counter to make tests simpler.
78      * (Some of the tests involve multiple connections but only have a single counter to track the
79      * tag.)
80      */
81     private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
82 
83     // Keep others from instantiating directly
ImapConnection(ImapStore store)84     ImapConnection(ImapStore store) {
85         setStore(store);
86     }
87 
setStore(ImapStore store)88     void setStore(ImapStore store) {
89         // TODO: maybe we should throw an exception if the connection is not closed here,
90         // if it's not currently closed, then we won't reopen it, so if the credentials have
91         // changed, the connection will not be reestablished.
92         mImapStore = store;
93         mLoginPhrase = null;
94     }
95 
96     /**
97      * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
98      * username and password, or an OAUTH authentication string, with username and access token.
99      * Currently, these are the only two auth mechanisms supported.
100      *
101      * @throws IOException
102      * @throws AuthenticationFailedException
103      * @return the login command string to sent to the IMAP server
104      */
getLoginPhrase()105     String getLoginPhrase() throws MessagingException, IOException {
106         // build the LOGIN string once (instead of over-and-over again.)
107         if (mImapStore.getUseOAuth()) {
108             // We'll recreate the login phrase if it's null, or if the access token
109             // has changed.
110             final String accessToken = AuthenticationCache.getInstance().retrieveAccessToken(
111                     mImapStore.getContext(), mImapStore.getAccount());
112             if (mLoginPhrase == null || !TextUtils.equals(mAccessToken, accessToken)) {
113                 mAccessToken = accessToken;
114                 final String oauthCode = "user=" + mImapStore.getUsername() + '\001' +
115                         "auth=Bearer " + mAccessToken + '\001' + '\001';
116                 mLoginPhrase = ImapConstants.AUTHENTICATE + " " + ImapConstants.XOAUTH2 + " " +
117                         Base64.encodeToString(oauthCode.getBytes(), Base64.NO_WRAP);
118             }
119         } else {
120             if (mLoginPhrase == null) {
121                 if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
122                     // build the LOGIN string once (instead of over-and-over again.)
123                     // apply the quoting here around the built-up password
124                     mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
125                             + ImapUtility.imapQuoted(mImapStore.getPassword());
126                 }
127             }
128         }
129         return mLoginPhrase;
130     }
131 
open()132     void open() throws IOException, MessagingException {
133         if (mTransport != null && mTransport.isOpen()) {
134             return;
135         }
136 
137         try {
138             // copy configuration into a clean transport, if necessary
139             if (mTransport == null) {
140                 mTransport = mImapStore.cloneTransport();
141             }
142 
143             mTransport.open();
144 
145             createParser();
146 
147             // BANNER
148             mParser.readResponse();
149 
150             // CAPABILITY
151             ImapResponse capabilities = queryCapabilities();
152 
153             boolean hasStartTlsCapability =
154                 capabilities.contains(ImapConstants.STARTTLS);
155 
156             // TLS
157             ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
158             if (newCapabilities != null) {
159                 capabilities = newCapabilities;
160             }
161 
162             // NOTE: An IMAP response MUST be processed before issuing any new IMAP
163             // requests. Subsequent requests may destroy previous response data. As
164             // such, we save away capability information here for future use.
165             setCapabilities(capabilities);
166             String capabilityString = capabilities.flatten();
167 
168             // ID
169             doSendId(isCapable(CAPABILITY_ID), capabilityString);
170 
171             // LOGIN
172             doLogin();
173 
174             // NAMESPACE (only valid in the Authenticated state)
175             doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
176 
177             // Gets the path separator from the server
178             doGetPathSeparator();
179 
180             mImapStore.ensurePrefixIsValid();
181         } catch (SSLException e) {
182             if (MailActivityEmail.DEBUG) {
183                 LogUtils.d(Logging.LOG_TAG, e, "SSLException");
184             }
185             throw new CertificateValidationException(e.getMessage(), e);
186         } catch (IOException ioe) {
187             // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
188             // of other code here that catches IOException and I don't want to break it.
189             // This catch is only here to enhance logging of connection-time issues.
190             if (MailActivityEmail.DEBUG) {
191                 LogUtils.d(Logging.LOG_TAG, ioe, "IOException");
192             }
193             throw ioe;
194         } finally {
195             destroyResponses();
196         }
197     }
198 
199     /**
200      * Closes the connection and releases all resources. This connection can not be used again
201      * until {@link #setStore(ImapStore)} is called.
202      */
close()203     void close() {
204         if (mTransport != null) {
205             mTransport.close();
206             mTransport = null;
207         }
208         destroyResponses();
209         mParser = null;
210         mImapStore = null;
211     }
212 
213     /**
214      * Returns whether or not the specified capability is supported by the server.
215      */
isCapable(int capability)216     private boolean isCapable(int capability) {
217         return (mCapabilities & capability) != 0;
218     }
219 
220     /**
221      * Sets the capability flags according to the response provided by the server.
222      * Note: We only set the capability flags that we are interested in. There are many IMAP
223      * capabilities that we do not track.
224      */
setCapabilities(ImapResponse capabilities)225     private void setCapabilities(ImapResponse capabilities) {
226         if (capabilities.contains(ImapConstants.ID)) {
227             mCapabilities |= CAPABILITY_ID;
228         }
229         if (capabilities.contains(ImapConstants.NAMESPACE)) {
230             mCapabilities |= CAPABILITY_NAMESPACE;
231         }
232         if (capabilities.contains(ImapConstants.UIDPLUS)) {
233             mCapabilities |= CAPABILITY_UIDPLUS;
234         }
235         if (capabilities.contains(ImapConstants.STARTTLS)) {
236             mCapabilities |= CAPABILITY_STARTTLS;
237         }
238     }
239 
240     /**
241      * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
242      * set it to {@link #mParser}.
243      *
244      * If we already have an {@link ImapResponseParser}, we
245      * {@link #destroyResponses()} and throw it away.
246      */
createParser()247     private void createParser() {
248         destroyResponses();
249         mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
250     }
251 
destroyResponses()252     void destroyResponses() {
253         if (mParser != null) {
254             mParser.destroyResponses();
255         }
256     }
257 
isTransportOpenForTest()258     boolean isTransportOpenForTest() {
259         return mTransport != null && mTransport.isOpen();
260     }
261 
readResponse()262     ImapResponse readResponse() throws IOException, MessagingException {
263         return mParser.readResponse();
264     }
265 
266     /**
267      * Send a single command to the server.  The command will be preceded by an IMAP command
268      * tag and followed by \r\n (caller need not supply them).
269      *
270      * @param command The command to send to the server
271      * @param sensitive If true, the command will not be logged
272      * @return Returns the command tag that was sent
273      */
sendCommand(String command, boolean sensitive)274     String sendCommand(String command, boolean sensitive)
275             throws MessagingException, IOException {
276         LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command));
277         open();
278         return sendCommandInternal(command, sensitive);
279     }
280 
sendCommandInternal(String command, boolean sensitive)281     String sendCommandInternal(String command, boolean sensitive)
282             throws MessagingException, IOException {
283         if (mTransport == null) {
284             throw new IOException("Null transport");
285         }
286         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
287         String commandToSend = tag + " " + command;
288         mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
289         mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
290         return tag;
291     }
292 
293     /**
294      * Send a single, complex command to the server.  The command will be preceded by an IMAP
295      * command tag and followed by \r\n (caller need not supply them).  After each piece of the
296      * command, a response will be read which MUST be a continuation request.
297      *
298      * @param commands An array of Strings comprising the command to be sent to the server
299      * @return Returns the command tag that was sent
300      */
sendComplexCommand(List<String> commands, boolean sensitive)301     String sendComplexCommand(List<String> commands, boolean sensitive) throws MessagingException,
302             IOException {
303         open();
304         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
305         int len = commands.size();
306         for (int i = 0; i < len; i++) {
307             String commandToSend = commands.get(i);
308             // The first part of the command gets the tag
309             if (i == 0) {
310                 commandToSend = tag + " " + commandToSend;
311             } else {
312                 // Otherwise, read the response from the previous part of the command
313                 ImapResponse response = readResponse();
314                 // If it isn't a continuation request, that's an error
315                 if (!response.isContinuationRequest()) {
316                     throw new MessagingException("Expected continuation request");
317                 }
318             }
319             // Send the command
320             mTransport.writeLine(commandToSend, null);
321             mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
322         }
323         return tag;
324     }
325 
executeSimpleCommand(String command)326     List<ImapResponse> executeSimpleCommand(String command) throws IOException, MessagingException {
327         return executeSimpleCommand(command, false);
328     }
329 
330     /**
331      * Read and return all of the responses from the most recent command sent to the server
332      *
333      * @return a list of ImapResponses
334      * @throws IOException
335      * @throws MessagingException
336      */
getCommandResponses()337     List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
338         final List<ImapResponse> responses = new ArrayList<ImapResponse>();
339         ImapResponse response;
340         do {
341             response = mParser.readResponse();
342             responses.add(response);
343         } while (!response.isTagged());
344 
345         if (!response.isOk()) {
346             final String toString = response.toString();
347             final String alert = response.getAlertTextOrEmpty().getString();
348             final String responseCode = response.getResponseCodeOrEmpty().getString();
349             destroyResponses();
350 
351             // if the response code indicates an error occurred within the server, indicate that
352             if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
353                 throw new MessagingException(MessagingException.SERVER_ERROR, alert);
354             }
355 
356             throw new ImapException(toString, alert, responseCode);
357         }
358         return responses;
359     }
360 
361     /**
362      * Execute a simple command at the server, a simple command being one that is sent in a single
363      * line of text
364      *
365      * @param command the command to send to the server
366      * @param sensitive whether the command should be redacted in logs (used for login)
367      * @return a list of ImapResponses
368      * @throws IOException
369      * @throws MessagingException
370      */
executeSimpleCommand(String command, boolean sensitive)371      List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
372             throws IOException, MessagingException {
373          // TODO: It may be nice to catch IOExceptions and close the connection here.
374          // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
375          sendCommand(command, sensitive);
376          return getCommandResponses();
377     }
378 
379      /**
380       * Execute a complex command at the server, a complex command being one that must be sent in
381       * multiple lines due to the use of string literals
382       *
383       * @param commands a list of strings that comprise the command to be sent to the server
384       * @param sensitive whether the command should be redacted in logs (used for login)
385       * @return a list of ImapResponses
386       * @throws IOException
387       * @throws MessagingException
388       */
executeComplexCommand(List<String> commands, boolean sensitive)389       List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
390             throws IOException, MessagingException {
391           sendComplexCommand(commands, sensitive);
392           return getCommandResponses();
393       }
394 
395     /**
396      * Query server for capabilities.
397      */
queryCapabilities()398     private ImapResponse queryCapabilities() throws IOException, MessagingException {
399         ImapResponse capabilityResponse = null;
400         for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
401             if (r.is(0, ImapConstants.CAPABILITY)) {
402                 capabilityResponse = r;
403                 break;
404             }
405         }
406         if (capabilityResponse == null) {
407             throw new MessagingException("Invalid CAPABILITY response received");
408         }
409         return capabilityResponse;
410     }
411 
412     /**
413      * Sends client identification information to the IMAP server per RFC 2971. If
414      * the server does not support the ID command, this will perform no operation.
415      *
416      * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
417      * malformed response that our parser can't deal with.
418      */
doSendId(boolean hasIdCapability, String capabilities)419     private void doSendId(boolean hasIdCapability, String capabilities)
420             throws MessagingException {
421         if (!hasIdCapability) return;
422 
423         // Never send ID to *.secureserver.net
424         String host = mTransport.getHost();
425         if (host.toLowerCase().endsWith(".secureserver.net")) return;
426 
427         // Assign user-agent string (for RFC2971 ID command)
428         String mUserAgent =
429                 ImapStore.getImapId(mImapStore.getContext(), mImapStore.getUsername(), host,
430                         capabilities);
431 
432         if (mUserAgent != null) {
433             mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
434         } else if (DEBUG_FORCE_SEND_ID) {
435             mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
436         }
437         // else: mIdPhrase = null, no ID will be emitted
438 
439         // Send user-agent in an RFC2971 ID command
440         if (mIdPhrase != null) {
441             try {
442                 executeSimpleCommand(mIdPhrase);
443             } catch (ImapException ie) {
444                 // Log for debugging, but this is not a fatal problem.
445                 if (MailActivityEmail.DEBUG) {
446                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
447                 }
448             } catch (IOException ioe) {
449                 // Special case to handle malformed OK responses and ignore them.
450                 // A true IOException will recur on the following login steps
451                 // This can go away after the parser is fixed - see bug 2138981
452             }
453         }
454     }
455 
456     /**
457      * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
458      * explicitly sets a namespace (using setup UI) or if the server does not support the
459      * namespace command, this will perform no operation.
460      */
doGetNamespace(boolean hasNamespaceCapability)461     private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
462         // user did not specify a hard-coded prefix; try to get it from the server
463         if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
464             List<ImapResponse> responseList = Collections.emptyList();
465 
466             try {
467                 responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
468             } catch (ImapException ie) {
469                 // Log for debugging, but this is not a fatal problem.
470                 if (MailActivityEmail.DEBUG) {
471                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
472                 }
473             } catch (IOException ioe) {
474                 // Special case to handle malformed OK responses and ignore them.
475             }
476 
477             for (ImapResponse response: responseList) {
478                 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
479                     ImapList namespaceList = response.getListOrEmpty(1);
480                     ImapList namespace = namespaceList.getListOrEmpty(0);
481                     String namespaceString = namespace.getStringOrEmpty(0).getString();
482                     if (!TextUtils.isEmpty(namespaceString)) {
483                         mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
484                         mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
485                     }
486                 }
487             }
488         }
489     }
490 
491     /**
492      * Logs into the IMAP server
493      */
doLogin()494     private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
495         try {
496             if (mImapStore.getUseOAuth()) {
497                 // SASL authentication can take multiple steps. Currently the only SASL
498                 // authentication supported is OAuth.
499                 doSASLAuth();
500             } else {
501                 executeSimpleCommand(getLoginPhrase(), true);
502             }
503         } catch (ImapException ie) {
504             if (MailActivityEmail.DEBUG) {
505                 LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
506             }
507 
508             final String code = ie.getResponseCode();
509             final String alertText = ie.getAlertText();
510 
511             // if the response code indicates expired or bad credentials, throw a special exception
512             if (ImapConstants.AUTHENTICATIONFAILED.equals(code) ||
513                     ImapConstants.EXPIRED.equals(code)) {
514                 throw new AuthenticationFailedException(alertText, ie);
515             }
516 
517             throw new MessagingException(alertText, ie);
518         }
519     }
520 
521     /**
522      * Performs an SASL authentication. Currently, the only type of SASL authentication supported
523      * is OAuth.
524      * @throws MessagingException
525      * @throws IOException
526      */
doSASLAuth()527     private void doSASLAuth() throws MessagingException, IOException {
528         LogUtils.d(Logging.LOG_TAG, "doSASLAuth");
529         ImapResponse response = getOAuthResponse();
530         if (!response.isOk()) {
531             // Failed to authenticate. This may be just due to an expired token.
532             LogUtils.d(Logging.LOG_TAG, "failed to authenticate, retrying");
533             destroyResponses();
534             // Clear the login phrase, this will force us to refresh the auth token.
535             mLoginPhrase = null;
536             // Close the transport so that we'll retry the authentication.
537             if (mTransport != null) {
538                 mTransport.close();
539                 mTransport = null;
540             }
541             response = getOAuthResponse();
542             if (!response.isOk()) {
543                 LogUtils.d(Logging.LOG_TAG, "failed to authenticate, giving up");
544                 destroyResponses();
545                 throw new AuthenticationFailedException("OAuth failed after refresh");
546             }
547         }
548     }
549 
getOAuthResponse()550     private ImapResponse getOAuthResponse() throws IOException, MessagingException {
551         ImapResponse response;
552         sendCommandInternal(getLoginPhrase(), true);
553         do {
554             response = mParser.readResponse();
555         } while (!response.isTagged() && !response.isContinuationRequest());
556 
557         if (response.isContinuationRequest()) {
558             // SASL allows for a challenge/response type authentication, so if it doesn't yet have
559             // enough info, it will send back a continuation request.
560             // Currently, the only type of authentication we support is OAuth. The only case where
561             // it will send a continuation request is when we fail to authenticate. We need to
562             // reply with a CR/LF, and it will then return with a NO response.
563             sendCommandInternal("", true);
564             response = readResponse();
565         }
566 
567         // if the response code indicates an error occurred within the server, indicate that
568         final String responseCode = response.getResponseCodeOrEmpty().getString();
569         if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
570             final String alert = response.getAlertTextOrEmpty().getString();
571             throw new MessagingException(MessagingException.SERVER_ERROR, alert);
572         }
573 
574         return response;
575     }
576 
577     /**
578      * Gets the path separator per the LIST command in RFC 3501. If the path separator
579      * was obtained while obtaining the namespace or there is no prefix defined, this
580      * will perform no operation.
581      */
doGetPathSeparator()582     private void doGetPathSeparator() throws MessagingException {
583         // user did not specify a hard-coded prefix; try to get it from the server
584         if (mImapStore.isUserPrefixSet()) {
585             List<ImapResponse> responseList = Collections.emptyList();
586 
587             try {
588                 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
589             } catch (ImapException ie) {
590                 // Log for debugging, but this is not a fatal problem.
591                 if (MailActivityEmail.DEBUG) {
592                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
593                 }
594             } catch (IOException ioe) {
595                 // Special case to handle malformed OK responses and ignore them.
596             }
597 
598             for (ImapResponse response: responseList) {
599                 if (response.isDataResponse(0, ImapConstants.LIST)) {
600                     mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
601                 }
602             }
603         }
604     }
605 
606     /**
607      * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
608      * to use TLS or the server does not support the TLS capability, this will perform
609      * no operation.
610      */
doStartTls(boolean hasStartTlsCapability)611     private ImapResponse doStartTls(boolean hasStartTlsCapability)
612             throws IOException, MessagingException {
613         if (mTransport.canTryTlsSecurity()) {
614             if (hasStartTlsCapability) {
615                 // STARTTLS
616                 executeSimpleCommand(ImapConstants.STARTTLS);
617 
618                 mTransport.reopenTls();
619                 createParser();
620                 // Per RFC requirement (3501-6.2.1) gather new capabilities
621                 return(queryCapabilities());
622             } else {
623                 if (MailActivityEmail.DEBUG) {
624                     LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
625                 }
626                 throw new MessagingException(MessagingException.TLS_REQUIRED);
627             }
628         }
629         return null;
630     }
631 
632     /** @see DiscourseLogger#logLastDiscourse() */
logLastDiscourse()633     void logLastDiscourse() {
634         mDiscourse.logLastDiscourse();
635     }
636 }
637