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