AuthenticatingSMTPClient.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.apache.commons.net.smtp;

  18. import java.io.IOException;
  19. import java.net.InetAddress;
  20. import java.security.InvalidKeyException;
  21. import java.security.NoSuchAlgorithmException;
  22. import java.security.spec.InvalidKeySpecException;
  23. import java.util.Arrays;
  24. import java.util.Base64;

  25. import javax.crypto.Mac;
  26. import javax.crypto.spec.SecretKeySpec;
  27. import javax.net.ssl.SSLContext;

  28. /**
  29.  * An SMTP Client class with authentication support (RFC4954).
  30.  *
  31.  * @see SMTPClient
  32.  * @since 3.0
  33.  */
  34. public class AuthenticatingSMTPClient extends SMTPSClient {

  35.     /**
  36.      * The enumeration of currently-supported authentication methods.
  37.      */
  38.     public enum AUTH_METHOD {

  39.         /** The standardized (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
  40.         PLAIN,

  41.         /** The standardized (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
  42.         CRAM_MD5,

  43.         /** The non-standarized Microsoft LOGIN method, which sends the password unencrypted (insecure). */
  44.         LOGIN,

  45.         /** XOAuth method which accepts a signed and base64ed OAuth URL. */
  46.         XOAUTH,

  47.         /** XOAuth 2 method which accepts a signed and base64ed OAuth JSON. */
  48.         XOAUTH2;

  49.         /**
  50.          * Gets the name of the given authentication method suitable for the server.
  51.          *
  52.          * @param method The authentication method to get the name for.
  53.          * @return The name of the given authentication method suitable for the server.
  54.          */
  55.         public static final String getAuthName(final AUTH_METHOD method) {
  56.             if (method.equals(AUTH_METHOD.PLAIN)) {
  57.                 return "PLAIN";
  58.             }
  59.             if (method.equals(AUTH_METHOD.CRAM_MD5)) {
  60.                 return "CRAM-MD5";
  61.             }
  62.             if (method.equals(AUTH_METHOD.LOGIN)) {
  63.                 return "LOGIN";
  64.             }
  65.             if (method.equals(AUTH_METHOD.XOAUTH)) {
  66.                 return "XOAUTH";
  67.             }
  68.             if (method.equals(AUTH_METHOD.XOAUTH2)) {
  69.                 return "XOAUTH2";
  70.             }
  71.             return null;
  72.         }
  73.     }

  74.     /** {@link Mac} algorithm. */
  75.     private static final String MAC_ALGORITHM = "HmacMD5";

  76.     /**
  77.      * The default AuthenticatingSMTPClient constructor. Creates a new Authenticating SMTP Client.
  78.      */
  79.     public AuthenticatingSMTPClient() {
  80.     }

  81.     /**
  82.      * Overloaded constructor that takes the implicit argument, and using {@link #DEFAULT_PROTOCOL} i.e. TLS
  83.      *
  84.      * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
  85.      * @param ctx      A pre-configured SSL Context.
  86.      * @since 3.3
  87.      */
  88.     public AuthenticatingSMTPClient(final boolean implicit, final SSLContext ctx) {
  89.         super(implicit, ctx);
  90.     }

  91.     /**
  92.      * Overloaded constructor that takes a protocol specification
  93.      *
  94.      * @param protocol The protocol to use
  95.      */
  96.     public AuthenticatingSMTPClient(final String protocol) {
  97.         super(protocol);
  98.     }

  99.     /**
  100.      * Overloaded constructor that takes a protocol specification and the implicit argument
  101.      *
  102.      * @param proto    the protocol.
  103.      * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
  104.      * @since 3.3
  105.      */
  106.     public AuthenticatingSMTPClient(final String proto, final boolean implicit) {
  107.         super(proto, implicit);
  108.     }

  109.     /**
  110.      * Overloaded constructor that takes the protocol specification, the implicit argument and encoding
  111.      *
  112.      * @param proto    the protocol.
  113.      * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
  114.      * @param encoding the encoding
  115.      * @since 3.3
  116.      */
  117.     public AuthenticatingSMTPClient(final String proto, final boolean implicit, final String encoding) {
  118.         super(proto, implicit, encoding);
  119.     }

  120.     /**
  121.      * Overloaded constructor that takes a protocol specification and encoding
  122.      *
  123.      * @param protocol The protocol to use
  124.      * @param encoding The encoding to use
  125.      * @since 3.3
  126.      */
  127.     public AuthenticatingSMTPClient(final String protocol, final String encoding) {
  128.         super(protocol, false, encoding);
  129.     }

  130.     /**
  131.      * Authenticate to the SMTP server by sending the AUTH command with the selected mechanism, using the given user and the given password.
  132.      *
  133.      * @param method   the method to use, one of the {@link AuthenticatingSMTPClient.AUTH_METHOD} enum values
  134.      * @param user the user name. If the method is XOAUTH/XOAUTH2, then this is used as the plain text oauth protocol parameter string which is
  135.      *                 Base64-encoded for transmission.
  136.      * @param password the password for the username. Ignored for XOAUTH/XOAUTH2.
  137.      *
  138.      * @return True if successfully completed, false if not.
  139.      * @throws SMTPConnectionClosedException If the SMTP server prematurely closes the connection as a result of the client being idle or some other reason
  140.      *                                       causing the server to send SMTP reply code 421. This exception may be caught either as an IOException or
  141.      *                                       independently as itself.
  142.      * @throws IOException                   If an I/O error occurs while either sending a command to the server or receiving a reply from the server.
  143.      * @throws NoSuchAlgorithmException      If the CRAM hash algorithm cannot be instantiated by the Java runtime system.
  144.      * @throws InvalidKeyException           If the CRAM hash algorithm failed to use the given password.
  145.      * @throws InvalidKeySpecException       If the CRAM hash algorithm failed to use the given password.
  146.      */
  147.     public boolean auth(final AuthenticatingSMTPClient.AUTH_METHOD method, final String user, final String password)
  148.             throws IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException {
  149.         if (!SMTPReply.isPositiveIntermediate(sendCommand(SMTPCommand.AUTH, AUTH_METHOD.getAuthName(method)))) {
  150.             return false;
  151.         }

  152.         if (method.equals(AUTH_METHOD.PLAIN)) {
  153.             // the server sends an empty response ("334 "), so we don't have to read it.
  154.             return SMTPReply
  155.                     .isPositiveCompletion(sendCommand(Base64.getEncoder().encodeToString(("\000" + user + "\000" + password).getBytes(getCharset()))));
  156.         }
  157.         if (method.equals(AUTH_METHOD.CRAM_MD5)) {
  158.             // get the CRAM challenge
  159.             final byte[] serverChallenge = Base64.getDecoder().decode(getReplyString().substring(4).trim());
  160.             // get the Mac instance
  161.             final Mac hmacMd5 = Mac.getInstance(MAC_ALGORITHM);
  162.             hmacMd5.init(new SecretKeySpec(password.getBytes(getCharset()), MAC_ALGORITHM));
  163.             // compute the result:
  164.             final byte[] hmacResult = convertToHexString(hmacMd5.doFinal(serverChallenge)).getBytes(getCharset());
  165.             // join the byte arrays to form the reply
  166.             final byte[] userNameBytes = user.getBytes(getCharset());
  167.             final byte[] toEncode = new byte[userNameBytes.length + 1 /* the space */ + hmacResult.length];
  168.             System.arraycopy(userNameBytes, 0, toEncode, 0, userNameBytes.length);
  169.             toEncode[userNameBytes.length] = ' ';
  170.             System.arraycopy(hmacResult, 0, toEncode, userNameBytes.length + 1, hmacResult.length);
  171.             // send the reply and read the server code:
  172.             return SMTPReply.isPositiveCompletion(sendCommand(Base64.getEncoder().encodeToString(toEncode)));
  173.         }
  174.         if (method.equals(AUTH_METHOD.LOGIN)) {
  175.             // the server sends fixed responses (base64("UserName") and
  176.             // base64("Password")), so we don't have to read them.
  177.             if (!SMTPReply.isPositiveIntermediate(sendCommand(Base64.getEncoder().encodeToString(user.getBytes(getCharset()))))) {
  178.                 return false;
  179.             }
  180.             return SMTPReply.isPositiveCompletion(sendCommand(Base64.getEncoder().encodeToString(password.getBytes(getCharset()))));
  181.         }
  182.         if (method.equals(AUTH_METHOD.XOAUTH) || method.equals(AUTH_METHOD.XOAUTH2)) {
  183.             return SMTPReply.isPositiveIntermediate(sendCommand(Base64.getEncoder().encodeToString(user.getBytes(getCharset()))));
  184.         }
  185.         return false; // safety check
  186.     }

  187.     /**
  188.      * Converts the given byte array to a String containing the hexadecimal values of the bytes. For example, the byte 'A' will be converted to '41', because
  189.      * this is the ASCII code (and the byte value) of the capital letter 'A'.
  190.      *
  191.      * @param a The byte array to convert.
  192.      * @return The resulting String of hexadecimal codes.
  193.      */
  194.     private String convertToHexString(final byte[] a) {
  195.         final StringBuilder result = new StringBuilder(a.length * 2);
  196.         for (final byte element : a) {
  197.             if ((element & 0x0FF) <= 15) {
  198.                 result.append("0");
  199.             }
  200.             result.append(Integer.toHexString(element & 0x0FF));
  201.         }
  202.         return result.toString();
  203.     }

  204.     /**
  205.      * A convenience method to send the ESMTP EHLO command to the server, receive the reply, and return the reply code.
  206.      *
  207.      * @param hostname The hostname of the sender.
  208.      * @return The reply code received from the server.
  209.      * @throws SMTPConnectionClosedException If the SMTP server prematurely closes the connection as a result of the client being idle or some other reason
  210.      *                                       causing the server to send SMTP reply code 421. This exception may be caught either as an IOException or
  211.      *                                       independently as itself.
  212.      * @throws IOException                   If an I/O error occurs while either sending the command or receiving the server reply.
  213.      */
  214.     public int ehlo(final String hostname) throws IOException {
  215.         return sendCommand(SMTPCommand.EHLO, hostname);
  216.     }

  217.     /**
  218.      * Login to the ESMTP server by sending the EHLO command with the client hostname as an argument. Before performing any mail commands, you must first login.
  219.      *
  220.      * @return True if successfully completed, false if not.
  221.      * @throws SMTPConnectionClosedException If the SMTP server prematurely closes the connection as a result of the client being idle or some other reason
  222.      *                                       causing the server to send SMTP reply code 421. This exception may be caught either as an IOException or
  223.      *                                       independently as itself.
  224.      * @throws IOException                   If an I/O error occurs while either sending a command to the server or receiving a reply from the server.
  225.      */
  226.     public boolean elogin() throws IOException {
  227.         final String name;
  228.         final InetAddress host;

  229.         host = getLocalAddress();
  230.         name = host.getHostName();

  231.         if (name == null) {
  232.             return false;
  233.         }

  234.         return SMTPReply.isPositiveCompletion(ehlo(name));
  235.     }

  236.     /**
  237.      * Login to the ESMTP server by sending the EHLO command with the given hostname as an argument. Before performing any mail commands, you must first login.
  238.      *
  239.      * @param hostname The hostname with which to greet the SMTP server.
  240.      * @return True if successfully completed, false if not.
  241.      * @throws SMTPConnectionClosedException If the SMTP server prematurely closes the connection as a result of the client being idle or some other reason
  242.      *                                       causing the server to send SMTP reply code 421. This exception may be caught either as an IOException or
  243.      *                                       independently as itself.
  244.      * @throws IOException                   If an I/O error occurs while either sending a command to the server or receiving a reply from the server.
  245.      */
  246.     public boolean elogin(final String hostname) throws IOException {
  247.         return SMTPReply.isPositiveCompletion(ehlo(hostname));
  248.     }

  249.     /**
  250.      * Returns the integer values of the enhanced reply code of the last SMTP reply.
  251.      *
  252.      * @return The integer values of the enhanced reply code of the last SMTP reply. First digit is in the first array element.
  253.      */
  254.     public int[] getEnhancedReplyCode() {
  255.         final String reply = getReplyString().substring(4);
  256.         final String[] parts = reply.substring(0, reply.indexOf(' ')).split("\\.");
  257.         final int[] res = new int[parts.length];
  258.         Arrays.setAll(res, i -> Integer.parseInt(parts[i]));
  259.         return res;
  260.     }
  261. }