PageRenderTime 64ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 0ms

/src/com/android/email/mail/transport/SmtpSender.java

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