PageRenderTime 135ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

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

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