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.DebugUtils;
23 import com.android.email.mail.internet.AuthenticationCache;
24 import com.android.email.mail.store.ImapStore.ImapException;
25 import com.android.email.mail.store.imap.ImapConstants;
26 import com.android.email.mail.store.imap.ImapList;
27 import com.android.email.mail.store.imap.ImapResponse;
28 import com.android.email.mail.store.imap.ImapResponseParser;
29 import com.android.email.mail.store.imap.ImapUtility;
30 import com.android.email.mail.transport.DiscourseLogger;
31 import com.android.email.mail.transport.MailTransport;
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 (DebugUtils.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 (DebugUtils.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 status = response.getStatusOrEmpty().getString();
348             final String alert = response.getAlertTextOrEmpty().getString();
349             final String responseCode = response.getResponseCodeOrEmpty().getString();
350             destroyResponses();
351 
352             // if the response code indicates an error occurred within the server, indicate that
353             if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
354                 throw new MessagingException(MessagingException.SERVER_ERROR, alert);
355             }
356 
357             throw new ImapException(toString, status, alert, responseCode);
358         }
359         return responses;
360     }
361 
362     /**
363      * Execute a simple command at the server, a simple command being one that is sent in a single
364      * line of text
365      *
366      * @param command the command to send to the server
367      * @param sensitive whether the command should be redacted in logs (used for login)
368      * @return a list of ImapResponses
369      * @throws IOException
370      * @throws MessagingException
371      */
executeSimpleCommand(String command, boolean sensitive)372      List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
373             throws IOException, MessagingException {
374          // TODO: It may be nice to catch IOExceptions and close the connection here.
375          // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
376          sendCommand(command, sensitive);
377          return getCommandResponses();
378     }
379 
380      /**
381       * Execute a complex command at the server, a complex command being one that must be sent in
382       * multiple lines due to the use of string literals
383       *
384       * @param commands a list of strings that comprise the command to be sent to the server
385       * @param sensitive whether the command should be redacted in logs (used for login)
386       * @return a list of ImapResponses
387       * @throws IOException
388       * @throws MessagingException
389       */
executeComplexCommand(List<String> commands, boolean sensitive)390       List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
391             throws IOException, MessagingException {
392           sendComplexCommand(commands, sensitive);
393           return getCommandResponses();
394       }
395 
396     /**
397      * Query server for capabilities.
398      */
queryCapabilities()399     private ImapResponse queryCapabilities() throws IOException, MessagingException {
400         ImapResponse capabilityResponse = null;
401         for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
402             if (r.is(0, ImapConstants.CAPABILITY)) {
403                 capabilityResponse = r;
404                 break;
405             }
406         }
407         if (capabilityResponse == null) {
408             throw new MessagingException("Invalid CAPABILITY response received");
409         }
410         return capabilityResponse;
411     }
412 
413     /**
414      * Sends client identification information to the IMAP server per RFC 2971. If
415      * the server does not support the ID command, this will perform no operation.
416      *
417      * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
418      * malformed response that our parser can't deal with.
419      */
doSendId(boolean hasIdCapability, String capabilities)420     private void doSendId(boolean hasIdCapability, String capabilities)
421             throws MessagingException {
422         if (!hasIdCapability) return;
423 
424         // Never send ID to *.secureserver.net
425         String host = mTransport.getHost();
426         if (host.toLowerCase().endsWith(".secureserver.net")) return;
427 
428         // Assign user-agent string (for RFC2971 ID command)
429         String mUserAgent =
430                 ImapStore.getImapId(mImapStore.getContext(), mImapStore.getUsername(), host,
431                         capabilities);
432 
433         if (mUserAgent != null) {
434             mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
435         } else if (DEBUG_FORCE_SEND_ID) {
436             mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
437         }
438         // else: mIdPhrase = null, no ID will be emitted
439 
440         // Send user-agent in an RFC2971 ID command
441         if (mIdPhrase != null) {
442             try {
443                 executeSimpleCommand(mIdPhrase);
444             } catch (ImapException ie) {
445                 // Log for debugging, but this is not a fatal problem.
446                 if (DebugUtils.DEBUG) {
447                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
448                 }
449             } catch (IOException ioe) {
450                 // Special case to handle malformed OK responses and ignore them.
451                 // A true IOException will recur on the following login steps
452                 // This can go away after the parser is fixed - see bug 2138981
453             }
454         }
455     }
456 
457     /**
458      * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
459      * explicitly sets a namespace (using setup UI) or if the server does not support the
460      * namespace command, this will perform no operation.
461      */
doGetNamespace(boolean hasNamespaceCapability)462     private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
463         // user did not specify a hard-coded prefix; try to get it from the server
464         if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
465             List<ImapResponse> responseList = Collections.emptyList();
466 
467             try {
468                 responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
469             } catch (ImapException ie) {
470                 // Log for debugging, but this is not a fatal problem.
471                 if (DebugUtils.DEBUG) {
472                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
473                 }
474             } catch (IOException ioe) {
475                 // Special case to handle malformed OK responses and ignore them.
476             }
477 
478             for (ImapResponse response: responseList) {
479                 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
480                     ImapList namespaceList = response.getListOrEmpty(1);
481                     ImapList namespace = namespaceList.getListOrEmpty(0);
482                     String namespaceString = namespace.getStringOrEmpty(0).getString();
483                     if (!TextUtils.isEmpty(namespaceString)) {
484                         mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
485                         mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
486                     }
487                 }
488             }
489         }
490     }
491 
492     /**
493      * Logs into the IMAP server
494      */
doLogin()495     private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
496         try {
497             if (mImapStore.getUseOAuth()) {
498                 // SASL authentication can take multiple steps. Currently the only SASL
499                 // authentication supported is OAuth.
500                 doSASLAuth();
501             } else {
502                 executeSimpleCommand(getLoginPhrase(), true);
503             }
504         } catch (ImapException ie) {
505             if (DebugUtils.DEBUG) {
506                 LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
507             }
508 
509             final String status = ie.getStatus();
510             final String code = ie.getResponseCode();
511             final String alertText = ie.getAlertText();
512 
513             // if the response code indicates expired or bad credentials, throw a special exception
514             if (ImapConstants.AUTHENTICATIONFAILED.equals(code) ||
515                     ImapConstants.EXPIRED.equals(code) ||
516                     (ImapConstants.NO.equals(status) && TextUtils.isEmpty(code))) {
517                 throw new AuthenticationFailedException(alertText, ie);
518             }
519 
520             throw new MessagingException(alertText, ie);
521         }
522     }
523 
524     /**
525      * Performs an SASL authentication. Currently, the only type of SASL authentication supported
526      * is OAuth.
527      * @throws MessagingException
528      * @throws IOException
529      */
doSASLAuth()530     private void doSASLAuth() throws MessagingException, IOException {
531         LogUtils.d(Logging.LOG_TAG, "doSASLAuth");
532         ImapResponse response = getOAuthResponse();
533         if (!response.isOk()) {
534             // Failed to authenticate. This may be just due to an expired token.
535             LogUtils.d(Logging.LOG_TAG, "failed to authenticate, retrying");
536             destroyResponses();
537             // Clear the login phrase, this will force us to refresh the auth token.
538             mLoginPhrase = null;
539             // Close the transport so that we'll retry the authentication.
540             if (mTransport != null) {
541                 mTransport.close();
542                 mTransport = null;
543             }
544             response = getOAuthResponse();
545             if (!response.isOk()) {
546                 LogUtils.d(Logging.LOG_TAG, "failed to authenticate, giving up");
547                 destroyResponses();
548                 throw new AuthenticationFailedException("OAuth failed after refresh");
549             }
550         }
551     }
552 
getOAuthResponse()553     private ImapResponse getOAuthResponse() throws IOException, MessagingException {
554         ImapResponse response;
555         sendCommandInternal(getLoginPhrase(), true);
556         do {
557             response = mParser.readResponse();
558         } while (!response.isTagged() && !response.isContinuationRequest());
559 
560         if (response.isContinuationRequest()) {
561             // SASL allows for a challenge/response type authentication, so if it doesn't yet have
562             // enough info, it will send back a continuation request.
563             // Currently, the only type of authentication we support is OAuth. The only case where
564             // it will send a continuation request is when we fail to authenticate. We need to
565             // reply with a CR/LF, and it will then return with a NO response.
566             sendCommandInternal("", true);
567             response = readResponse();
568         }
569 
570         // if the response code indicates an error occurred within the server, indicate that
571         final String responseCode = response.getResponseCodeOrEmpty().getString();
572         if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
573             final String alert = response.getAlertTextOrEmpty().getString();
574             throw new MessagingException(MessagingException.SERVER_ERROR, alert);
575         }
576 
577         return response;
578     }
579 
580     /**
581      * Gets the path separator per the LIST command in RFC 3501. If the path separator
582      * was obtained while obtaining the namespace or there is no prefix defined, this
583      * will perform no operation.
584      */
doGetPathSeparator()585     private void doGetPathSeparator() throws MessagingException {
586         // user did not specify a hard-coded prefix; try to get it from the server
587         if (mImapStore.isUserPrefixSet()) {
588             List<ImapResponse> responseList = Collections.emptyList();
589 
590             try {
591                 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
592             } catch (ImapException ie) {
593                 // Log for debugging, but this is not a fatal problem.
594                 if (DebugUtils.DEBUG) {
595                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
596                 }
597             } catch (IOException ioe) {
598                 // Special case to handle malformed OK responses and ignore them.
599             }
600 
601             for (ImapResponse response: responseList) {
602                 if (response.isDataResponse(0, ImapConstants.LIST)) {
603                     mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
604                 }
605             }
606         }
607     }
608 
609     /**
610      * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
611      * to use TLS or the server does not support the TLS capability, this will perform
612      * no operation.
613      */
doStartTls(boolean hasStartTlsCapability)614     private ImapResponse doStartTls(boolean hasStartTlsCapability)
615             throws IOException, MessagingException {
616         if (mTransport.canTryTlsSecurity()) {
617             if (hasStartTlsCapability) {
618                 // STARTTLS
619                 executeSimpleCommand(ImapConstants.STARTTLS);
620 
621                 mTransport.reopenTls();
622                 createParser();
623                 // Per RFC requirement (3501-6.2.1) gather new capabilities
624                 return(queryCapabilities());
625             } else {
626                 if (DebugUtils.DEBUG) {
627                     LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
628                 }
629                 throw new MessagingException(MessagingException.TLS_REQUIRED);
630             }
631         }
632         return null;
633     }
634 
635     /** @see DiscourseLogger#logLastDiscourse() */
logLastDiscourse()636     void logLastDiscourse() {
637         mDiscourse.logLastDiscourse();
638     }
639 }
640