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