• 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.transport;
18 
19 import com.android.email.Email;
20 import com.android.email.codec.binary.Base64;
21 import com.android.email.mail.Address;
22 import com.android.email.mail.AuthenticationFailedException;
23 import com.android.email.mail.CertificateValidationException;
24 import com.android.email.mail.MessagingException;
25 import com.android.email.mail.Sender;
26 import com.android.email.mail.Transport;
27 import com.android.email.provider.EmailContent.Message;
28 
29 import android.content.Context;
30 import android.util.Config;
31 import android.util.Log;
32 
33 import java.io.IOException;
34 import java.net.InetAddress;
35 import java.net.URI;
36 import java.net.URISyntaxException;
37 
38 import javax.net.ssl.SSLException;
39 
40 /**
41  * This class handles all of the protocol-level aspects of sending messages via SMTP.
42  */
43 public class SmtpSender extends Sender {
44 
45     Context mContext;
46     private Transport mTransport;
47     String mUsername;
48     String mPassword;
49 
50     /**
51      * Static named constructor.
52      */
newInstance(Context context, String uri)53     public static Sender newInstance(Context context, String uri) throws MessagingException {
54         return new SmtpSender(context, uri);
55     }
56 
57     /**
58      * Allowed formats for the Uri:
59      * smtp://user:password@server:port
60      * smtp+tls+://user:password@server:port
61      * smtp+tls+trustallcerts://user:password@server:port
62      * smtp+ssl+://user:password@server:port
63      * smtp+ssl+trustallcerts://user:password@server:port
64      *
65      * @param uriString the Uri containing information to configure this sender
66      */
SmtpSender(Context context, String uriString)67     private SmtpSender(Context context, String uriString) throws MessagingException {
68         mContext = context;
69         URI uri;
70         try {
71             uri = new URI(uriString);
72         } catch (URISyntaxException use) {
73             throw new MessagingException("Invalid SmtpTransport URI", use);
74         }
75 
76         String scheme = uri.getScheme();
77         if (scheme == null || !scheme.startsWith("smtp")) {
78             throw new MessagingException("Unsupported protocol");
79         }
80         // defaults, which can be changed by security modifiers
81         int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
82         int defaultPort = 587;
83         // check for security modifiers and apply changes
84         if (scheme.contains("+ssl")) {
85             connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
86             defaultPort = 465;
87         } else if (scheme.contains("+tls")) {
88             connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
89         }
90         boolean trustCertificates = scheme.contains("+trustallcerts");
91 
92         mTransport = new MailTransport("SMTP");
93         mTransport.setUri(uri, defaultPort);
94         mTransport.setSecurity(connectionSecurity, trustCertificates);
95 
96         String[] userInfoParts = mTransport.getUserInfoParts();
97         if (userInfoParts != null) {
98             mUsername = userInfoParts[0];
99             if (userInfoParts.length > 1) {
100                 mPassword = userInfoParts[1];
101             }
102         }
103     }
104 
105     /**
106      * For testing only.  Injects a different transport.  The transport should already be set
107      * up and ready to use.  Do not use for real code.
108      * @param testTransport The Transport to inject and use for all future communication.
109      */
setTransport(Transport testTransport)110     /* package */ void setTransport(Transport testTransport) {
111         mTransport = testTransport;
112     }
113 
114     @Override
open()115     public void open() throws MessagingException {
116         try {
117             mTransport.open();
118 
119             // Eat the banner
120             executeSimpleCommand(null);
121 
122             String localHost = "localhost";
123             try {
124                 InetAddress localAddress = InetAddress.getLocalHost();
125                 localHost = localAddress.getHostName();
126             } catch (Exception e) {
127                 if (Config.LOGD && Email.DEBUG) {
128                     Log.d(Email.LOG_TAG, "Unable to look up localhost");
129                 }
130             }
131 
132             String result = executeSimpleCommand("EHLO " + localHost);
133 
134             /*
135              * TODO may need to add code to fall back to HELO I switched it from
136              * using HELO on non STARTTLS connections because of AOL's mail
137              * server. It won't let you use AUTH without EHLO.
138              * We should really be paying more attention to the capabilities
139              * and only attempting auth if it's available, and warning the user
140              * if not.
141              */
142             if (mTransport.canTryTlsSecurity()) {
143                 if (result.contains("-STARTTLS")) {
144                     executeSimpleCommand("STARTTLS");
145                     mTransport.reopenTls();
146                     /*
147                      * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
148                      * Exim.
149                      */
150                     result = executeSimpleCommand("EHLO " + localHost);
151                 } else {
152                     if (Config.LOGD && Email.DEBUG) {
153                         Log.d(Email.LOG_TAG, "TLS not supported but required");
154                     }
155                     throw new MessagingException(MessagingException.TLS_REQUIRED);
156                 }
157             }
158 
159             /*
160              * result contains the results of the EHLO in concatenated form
161              */
162             boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$");
163             boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$");
164 
165             if (mUsername != null && mUsername.length() > 0 && mPassword != null
166                     && mPassword.length() > 0) {
167                 if (authPlainSupported) {
168                     saslAuthPlain(mUsername, mPassword);
169                 }
170                 else if (authLoginSupported) {
171                     saslAuthLogin(mUsername, mPassword);
172                 }
173                 else {
174                     if (Config.LOGD && Email.DEBUG) {
175                         Log.d(Email.LOG_TAG, "No valid authentication mechanism found.");
176                     }
177                     throw new MessagingException(MessagingException.AUTH_REQUIRED);
178                 }
179             }
180         } catch (SSLException e) {
181             if (Config.LOGD && Email.DEBUG) {
182                 Log.d(Email.LOG_TAG, e.toString());
183             }
184             throw new CertificateValidationException(e.getMessage(), e);
185         } catch (IOException ioe) {
186             if (Config.LOGD && Email.DEBUG) {
187                 Log.d(Email.LOG_TAG, ioe.toString());
188             }
189             throw new MessagingException(MessagingException.IOERROR, ioe.toString());
190         }
191     }
192 
193     @Override
sendMessage(long messageId)194     public void sendMessage(long messageId) throws MessagingException {
195         close();
196         open();
197 
198         Message message = Message.restoreMessageWithId(mContext, messageId);
199         if (message == null) {
200             throw new MessagingException("Trying to send non-existent message id="
201                     + Long.toString(messageId));
202         }
203         Address from = Address.unpackFirst(message.mFrom);
204         Address[] to = Address.unpack(message.mTo);
205         Address[] cc = Address.unpack(message.mCc);
206         Address[] bcc = Address.unpack(message.mBcc);
207 
208         try {
209             executeSimpleCommand("MAIL FROM: " + "<" + from.getAddress() + ">");
210             for (Address address : to) {
211                 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
212             }
213             for (Address address : cc) {
214                 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
215             }
216             for (Address address : bcc) {
217                 executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
218             }
219             executeSimpleCommand("DATA");
220             // TODO byte stuffing
221             Rfc822Output.writeTo(mContext, messageId,
222                     new EOLConvertingOutputStream(mTransport.getOutputStream()), true, false);
223             executeSimpleCommand("\r\n.");
224         } catch (IOException ioe) {
225             throw new MessagingException("Unable to send message", ioe);
226         }
227     }
228 
229     /**
230      * Close the protocol (and the transport below it).
231      *
232      * MUST NOT return any exceptions.
233      */
234     @Override
close()235     public void close() {
236         mTransport.close();
237     }
238 
239     /**
240      * Send a single command and wait for a single response.  Handles responses that continue
241      * onto multiple lines.  Throws MessagingException if response code is 4xx or 5xx.  All traffic
242      * is logged (if debug logging is enabled) so do not use this function for user ID or password.
243      *
244      * @param command The command string to send to the server.
245      * @return Returns the response string from the server.
246      */
executeSimpleCommand(String command)247     private String executeSimpleCommand(String command) throws IOException, MessagingException {
248         return executeSensitiveCommand(command, null);
249     }
250 
251     /**
252      * Send a single command and wait for a single response.  Handles responses that continue
253      * onto multiple lines.  Throws MessagingException if response code is 4xx or 5xx.
254      *
255      * @param command The command string to send to the server.
256      * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
257      * please pass a replacement string here (for logging).
258      * @return Returns the response string from the server.
259      */
executeSensitiveCommand(String command, String sensitiveReplacement)260     private String executeSensitiveCommand(String command, String sensitiveReplacement)
261             throws IOException, MessagingException {
262         if (command != null) {
263             mTransport.writeLine(command, sensitiveReplacement);
264         }
265 
266         String line = mTransport.readLine();
267 
268         String result = line;
269 
270         while (line.length() >= 4 && line.charAt(3) == '-') {
271             line = mTransport.readLine();
272             result += line.substring(3);
273         }
274 
275         if (result.length() > 0) {
276             char c = result.charAt(0);
277             if ((c == '4') || (c == '5')) {
278                 throw new MessagingException(result);
279             }
280         }
281 
282         return result;
283     }
284 
285 
286 //    C: AUTH LOGIN
287 //    S: 334 VXNlcm5hbWU6
288 //    C: d2VsZG9u
289 //    S: 334 UGFzc3dvcmQ6
290 //    C: dzNsZDBu
291 //    S: 235 2.0.0 OK Authenticated
292 //
293 //    Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads:
294 //
295 //
296 //    C: AUTH LOGIN
297 //    S: 334 Username:
298 //    C: weldon
299 //    S: 334 Password:
300 //    C: w3ld0n
301 //    S: 235 2.0.0 OK Authenticated
302 
saslAuthLogin(String username, String password)303     private void saslAuthLogin(String username, String password) throws MessagingException,
304         AuthenticationFailedException, IOException {
305         try {
306             executeSimpleCommand("AUTH LOGIN");
307             executeSensitiveCommand(new String(Base64.encodeBase64(username.getBytes())),
308                     "/username redacted/");
309             executeSensitiveCommand(new String(Base64.encodeBase64(password.getBytes())),
310                     "/password redacted/");
311         }
312         catch (MessagingException me) {
313             if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
314                 throw new AuthenticationFailedException(me.getMessage());
315             }
316             throw me;
317         }
318     }
319 
saslAuthPlain(String username, String password)320     private void saslAuthPlain(String username, String password) throws MessagingException,
321             AuthenticationFailedException, IOException {
322         byte[] data = ("\000" + username + "\000" + password).getBytes();
323         data = new Base64().encode(data);
324         try {
325             executeSensitiveCommand("AUTH PLAIN " + new String(data), "AUTH PLAIN /redacted/");
326         }
327         catch (MessagingException me) {
328             if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
329                 throw new AuthenticationFailedException(me.getMessage());
330             }
331             throw me;
332         }
333     }
334 }
335