/**
 * BdkimAnalyzer.java
 * Copyright (c) 2011, Casey Connor
 * All rights reserved.
 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *    - Redistributions of source code must retain the above copyright notice, this
 *      list of conditions and the following disclaimer.
 *    - Redistributions in binary form must reproduce the above copyright notice,
 *      this list of conditions and the following disclaimer in the documentation
 *      and/or other materials provided with the distribution.
 *    - Neither the name of Lacinato nor the names of its contributors may be used to
 *      endorse or promote products derived from this software without specific prior
 *      written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 @author Casey Connor -- lacinato.com
 @version 1.0
 
 * For more information, see http://lacinato.com/cm/software/emailrelated/bdkim
 
 * A Java client to the BDKIM perl server process, this class provides convenience methods
 * for sending a message to the perl server and receiving the evaluated DomainKey and DKIM
 * results. It does not handle signing of messages, only verification.
 */

package com.lacinato.tools.bdkim;

import java.net.Socket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.StringReader;

/**
 * A Java client to the BDKIM perl server process, this class provides convenience methods
 * for sending a message to the perl server and receiving the evaluated DomainKey and DKIM
 * results. It does not handle signing of messages, only verification.
 <p/>
 * See determineDkimStatuses() for basic usage.
 <p/>
 * For more information, see <a href="http://lacinato.com/cm/software/emailrelated/bdkim">http://lacinato.com/cm/software/emailrelated/bdkim</a>
 <p/>
 @author Casey Connor -- <a href="http://lacinato.com">lacinato.com</a>
 @version 1.0
 */
public class BdkimAnalyzer
{
    /**
     * Default 12300
     */
    public static final int DEFAULT_PORT = 12300;
    /**
     * Default &quot;localhost&quot;
     */
    public static final String DEFAULT_HOST = "localhost";
    /**
     * Default 15 seconds
     */
    public static final int DEFAULT_CONNECTION_TIMEOUT = 15// seconds
    /**
     * Default 300 seconds - DNS calls could take a while on the perl side
     */
    public static final int DEFAULT_DATA_TIMEOUT = 300// seconds
    /**
     * Default 3 total attempts to connect to non-responsive server port
     */
    public static final int DEFAULT_CONNECT_TRIES = 3;

    private int port = DEFAULT_PORT;
    private String host = DEFAULT_HOST;
    private int connection_timeout = DEFAULT_CONNECTION_TIMEOUT * 1000;
    private int data_timeout = DEFAULT_DATA_TIMEOUT * 1000;
    private int connect_tries = DEFAULT_CONNECT_TRIES;
    private InetAddress connection_address = null;

    private boolean init_complete = false;

    // lower case, include :
    private static final String DKIM_HEADER_STRING = "dkim-signature:";
    private static final String DK_HEADER_STRING = "domainkey-signature:";

    /**
     * This is the main API call into BdkimAnalyzer.
     <p/>
     * Given a raw message (headers and all), this method will return the DK/DKIM results from the perl BDKIM server.
     * It will throw an AuthVerificationException (with explanatory copy inside it) on errors, timeouts,
     * etc. (Note that DKIM results of &quot;temperror&quot; et al. will not throw exceptions, but will return
     * as results in the SignatureResults object.) See the SignatureResults documentation for
     * important information about interpreting results.
     <p/>
     * Line endings are important in evaluating the DK/DKIM results of email messages.
     * Message lines are supposed to end with \r\n. This method will make sure the lines have those
     * endings before sending to the perl server, so you don't have to worry about it.
     <p/>
     * This class will do character encoding and decoding based on whatever the system character set is.
     * It is a good idea that it use the same character set as your perl-side BDKIM server or the
     * verification may have trouble.
     <p/>
     * On first use, this class will ensure that it can resolve the name of the server it will be connecting
     * to and throw an AuthVerificationException if it can not.
     <p/>
     * This version of determineDkimStatuses will reflect to determineDkimStatuses(String, boolean) with
     * &quot;true&quot; for the boolean.
     */
    public SignatureResults determineDkimStatuses(String raw_messagethrows AuthVerificationException
    return(determineDkimStatuses(raw_message, true))}

    /**
     * Given a raw message (headers and all), this method will return the DK/DKIM results from the perl BDKIM server.
     * It will throw an AuthVerificationException (with explanatory copy inside it) on errors, timeouts,
     * etc. (Note that DKIM &quot;permerror&quot; et al. will not throw exceptions, but will return
     * as results in the SignatureResults object.) See the SignatureResults documentation for
     * important information about interpreting results.
     <p/>
     * If &quot;check for headers&quot; is true, then this method will check the headers of the email
     * message for DomainKey and DKIM signatures. If it finds none, it will not bother to send the
     * messasge to the BDKIM server and will return an empty SignatureResults object. Use this feature
     * if you are verifying messages that may not have such headers in them. If you know that the messages
     * you are verifying have signatures in them, then you can pass this as &quot;false&quot; to save a
     * tiny bit of wasted effort. Note that whatever the setting of this boolean, the BDKIM perl server
     * does its own check of the headers before bothering to compute the results of the entire message
     * (see private method queryBDKIM() for more on that).
     <p/>
     * Line endings are important in evaluating the DK/DKIM results of email messages.
     * Message lines are supposed to end with \r\n. This method will make sure the lines have those
     * endings before sending to the perl server, so you don't have to worry about it.
     <p/>
     * This class will do character encoding and decoding based on whatever the system character set is.
     * It is a good idea that it use the same character set as your perl-side BDKIM server or the
     * verification may have trouble.
     <p/>
     * On first use, this class will ensure that it can resolve the name of the server it will be connecting
     * to and throw an AuthVerificationException if it can not.
     */
    public SignatureResults determineDkimStatuses(String raw_message, boolean check_for_headersthrows AuthVerificationException
    {
        if (raw_message == nullthrow new AuthVerificationException("determineDkimStatuses failed. Null message passed in.");

        String server_result = null;

        // if there are no headers, then there is no need to actually send to Mail::DKIM for the results
        if (check_for_headers && (!hasDkOrDkimHeaders(raw_message))) return(new SignatureResults());
        else
        {
            // DK or DKIM headers exist, or we were told not to check, so we need to hit the server.
            // true == use early-exit if no sigs exist or if they are malformed... only partially redundant with hasDkOrDkimHeaders()
            server_result = queryBdkim(raw_message, true);

            if (server_result == null || server_result.equals(""))
                throw new AuthVerificationException("determineDkimStatuses failed. Strange [result] from server: [" + server_result + "]");
        }

        return(interpretResults(server_result));
    }

    /**
     * This method is public as a possible convenience only. You don't need it, generally.
     
     @return true if the raw email message represented by message_txt contains any DomainKey or DKIM headers in the header section (case-insensitive)
     */
    public boolean hasDkOrDkimHeaders(String message_txt)
    {
        BufferedReader my_bufr = new BufferedReader(new StringReader(message_txt));
        String cur_header_line = null;

        while (true)
        {
            try cur_header_line = my_bufr.readLine()}
            catch (IOException ioe) { return(false)// would be weird

            if (cur_header_line != null)
            {
                cur_header_line = cur_header_line.toLowerCase();
                if (cur_header_line.startsWith(DKIM_HEADER_STRING|| cur_header_line.startsWith(DK_HEADER_STRING)) return(true);

                if (cur_header_line.equals("")) return(false)// got to header/body separator without finding a relevant header
            }
            else return(false)// strange, no message body found... oh well.
        }
    }

    /**
     * Given the response from the perl server, interpret the results and construct the SignatureResults object.
     */
    private SignatureResults interpretResults(String server_resultthrows AuthVerificationException
    {
        SignatureResults sig_res = null;

        if (server_result.startsWith("TE\n"))
            throw new AuthVerificationException("interpretResults failed. BDKIM reports error transmitting message: " +
                    server_result.substring(3, server_result.length() 1));
        else if (server_result.startsWith("TS\nDE\n"))
            throw new AuthVerificationException("interpretResults failed. BDKIM could not perform DKIM check: " +
                    server_result.substring(6, server_result.length() 1));
        else if (! server_result.startsWith("TS\nDS\n"))
            throw new AuthVerificationException("interpretResults failed. Strange results from BDKIM: [" +
                    server_result + "]");

        sig_res = SignatureResults.parse(server_result.substring(6, server_result.length() 1));

        if (sig_res == null)
            throw new AuthVerificationException("interpretResults failed. Strange results from BDKIM server, could not parse: [" +
                    server_result + "]");
        
        return(sig_res);
    }

    /**
     * Given the raw message text, send it to the BDKIM perl server and return what
     * comes back.
     
     * The boolean "use_post_headers_dkim_early_exit" determines whether or not
     * this method will send the appropriate protocol code to the BDKIM server telling it
     * that we are interested to send the headers first and then see if any early results
     * are possible. This is an efficiency tactic, since there may not be any signatures
     * present (if we haven't checked on the client side) or if the signatures themselves
     * are malformed. It is recommended that you pass in "true"
     
     * A couple things to note:
     
     * If use_post_headers_dkim_early_exit is true, then this method will clean up all line
     * endings in the message to "\r\n", which is the proper way in RFC-2822 and what the perl
     * server expects by default. If use_post_headers_dkim_early_exit is false,
     * it will pass in the message_text as it was sent in, so note that distinction.
     
     * This method will do character encoding and decoding based on whatever the system character set is.
     * It is a good idea that it use the same character set as your perl-side BDKIM server or the
     * verification may have trouble.
     */
    private String queryBdkim(String message_text, boolean use_post_headers_dkim_early_exitthrows AuthVerificationException
    {
        // init this class if needed
        if (! init_complete)
        {
            String s = init()// this method is synchronized for reentrant use
            if (s != nullthrow new AuthVerificationException("queryBdkim failed. Could not init(): " + s);
        }

        // connect to the server

        InetSocketAddress isa;
        Socket socket = new Socket();

        try
        {
            isa = new InetSocketAddress(connection_address, port);
        }
        catch (IllegalArgumentException iae)
        {
            throw new AuthVerificationException("queryBdkim failed. Could not create socket addr object " +
                                                "with connection address: " + connection_address + " on port: " + port);
        }

        int try_count = 1;

        while (try_count <= connect_tries)
        {
            try
            {
                socket.connect(isa, getConnectionTimeoutMillis());
            }
            catch (SocketTimeoutException ste)
            {
                // it'd be nice to log this fact
                try_count++;
                continue;
            }
            catch (IOException ioe)
            {
                throw new AuthVerificationException("queryBdkim failed. Could not connect socket, address: " + connection_address + " on port: " +
                                                    port + "; got ioe: " + ioe);
            }
            catch (Exception e)
            {
                throw new AuthVerificationException("queryBdkim failed. Could not connect socket, address: " + connection_address + " on port: " +
                                                    port + "; got strange exception: " + e);
            }

            // Ok, got a good socket connect, so break out:
            break;
        }

        if (! socket.isConnected())
        {
            try
            {
                socket.close();

                throw new AuthVerificationException("queryBdkim failed. Could not connect socket after " +
                                                    connect_tries + " attempts, address: " + connection_address + " on port: " +
                                                    port);
            }
            catch (IOException ioe)
            {
                throw new AuthVerificationException("queryBdkim failed. Could not close socket after this error: Could not connect socket after " +
                                                    connect_tries + " attempts, address: " + connection_address + " on port: " + port);
            }
        }

        // send query, retrieve result

        OutputStream out = null;
        BufferedReader in = null;
        StringBuffer response = new StringBuffer(4096);

        try
        {
            // NOTE! This will do encoding/decoding based on the system charset!

            socket.setSoTimeout(getDataTimeoutMillis());

            out = socket.getOutputStream();
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            String s;

            if (use_post_headers_dkim_early_exit)
            {
                // this branch of the method will fix all line endings

                // write the EE code
                out.write("EE\r\n".getBytes());

                // ...then write just the header part of the message
                BufferedReader my_bufr = new BufferedReader(new StringReader(message_text));

                String cur_header_line = null;
                boolean keep_going = true;

                while (keep_going)
                {
                    cur_header_line = my_bufr.readLine();
                    if (cur_header_line != null)
                    {
                        out.write((cur_header_line + "\r\n").getBytes());

                        if (cur_header_line.equals(""))
                        {
                            // found header/body separator
                            out.flush();
                            s = in.readLine();
                            if (s == nullthrow new AuthVerificationException("queryBdkim failed. " +
                                        "Could not get server response after headers sent in early exit mode. Got null.");
                            else if (s.equals("HC"))
                            {
                                // server wants the rest of the message
                                // note that we don't append "HC" to the "response" String from the server, for simplified parsing of results
                                while ((s = my_bufr.readLine()) != null)
                                {
                                    out.write((s + "\r\n").getBytes());
                                }
                            }
                            else
                            {
                                // server has (or should have) the results for us; assuming so, add this first line ("TS" or "TE")
                                // to the response String, we'll read in the rest when we fall through
                                response.append(s).append("\n");
                                keep_going = false;
                            }
                        }
                    }
                    else
                    {
                        keep_going = false// strange, no message body found... oh well.
                    }
                }
                out.flush();
                my_bufr.close();
            }
            else
            {
                // write the entire message
                // this branch of the method will not fix line endings!
                out.write(message_text.getBytes());
                out.flush();
            }

            socket.shutdownOutput();

            // read in the server results (or, more precisely, the rest of them, in the EE case)

            while ((s = in.readLine()) != null)
            {
                response.append(s).append("\n");
            }
        }
        catch (IllegalStateException ise)
        {
            throw new AuthVerificationException("queryBdkim failed. Could not get server response. Got ise: " + ise);
        }
        catch (IOException ioe)
        {
            throw new AuthVerificationException("queryBdkim failed. Could not get server response. Got iao: " + ioe);
        }
        finally
        {
            try
            {
                if (in != nullin.close();
                if (out != nullout.close();
                socket.close();
            }
            catch(IOException ioe)
            {
                throw new AuthVerificationException("queryBdkim failed. Could not close one of in, out, or socket. Got ioe: " + ioe);
            }
        }

        return(response.toString());
    }

    /**
     * Set up the client for use. Will make sure the host name resolves to an IP. Called automatically when needed.
     *
     @return non-null string on any errors
     */
    private String init()
    {
        InetAddress[] ias = null;

        try
        {
            ias = InetAddress.getAllByName(host);
        }
        catch (UnknownHostException uhe)
        {
            return("init failed: Couldn't resolve supplied host of [" + host + "], got uhe: " + uhe);
        }
        catch (SecurityException se)
        {
            return("init failed: Couldn't resolve supplied host of [" + host + "], got se: " + se);
        }

        if (ias.length != || ias[0== nullreturn("init failed: Got weird host array for host [" + host + "].");

        connection_address = ias[0];

        if (connection_timeout < 0return("init failed: connection_timeout must be > 0, it was: " + connection_timeout);

        init_complete = true;
        return(null);
    }

    /**
     * Port to use when contacting the BDKIM perl serevr -- must be between 0 and 65535, inclusive
     */
    public void setPort(int p) { port = p; init_complete = false}
    public int getPort() { return(port)}

    /**
     * host name, or IP address in String form, pointing to the BDKIM perl server,
     * e.g. &quot;localhost&quot; or &quot;127.0.0.1&quot; or &quot;123.123.123.123&quot;
     */
    public void setHost(String h) { host = h; init_complete = false}
    public String getHost() { return(host)}

    /**
     * Set the connection timeout value for connection to the BDKIM perl server, value
     * specified in seconds; this only applies when first creating the connection, not
     * during data transmission.
     */
    public void setConnectionTimeout(int s) { connection_timeout = s * 1000}
    /**
     @return the timeout value, in seconds
     */
    public int getConnectionTimeout() { return(connection_timeout/1000)}
    private int getConnectionTimeoutMillis() { return(connection_timeout)}

    /**
     * Set the connection timeout value for connection to the BDKIM perl server, value
     * specified in seconds; this only applies when sending data to the server, not
     * when making the original connection.
     */
    public void setDataTimeout(int s) { data_timeout = s * 1000}
    /**
     @return the timeout value, in seconds
     */
    public int getDataTimeout() { return(data_timeout/1000)}
    private int getDataTimeoutMillis() { return(data_timeout)}

    /**
     * Set the number of attempts to be made to connect to the server before
     * failing a given connection attempt (for one message)
     */
    public void setConnectTries(int t) { connect_tries = t; }
    public int getConnectTries() { return(connect_tries)}

    /**
     * A structured data object that holds the results of the DomainKey/DKIM evaluation.
     <p/>
     * A count of the number of signatures encountered is kept. Using getNumberOfSignatures(),
     * you can iterate through the results using the other access methods. The results are
     * indexed starting at 0.
     <p/>
     * Any signature, whether valid or not, will get an entry in this results object. As long
     * as the header name is present, it will be evaluated and a result will be found here.
     */
    public static class SignatureResults
    {
        private int num_sigs = 0;
        private String results[] null;
        private String headerlists[] null;
        private String domains[] null;

        /**
         * Creates an empty set of results (number of signaures == 0)
         */
        public SignatureResults()
        {
            num_sigs = 0;
            results = null;
            headerlists = null;
            domains = null;
        }

        /**
         * DomainKey and DKIM results. Note that Mail::DKIM will not return
         * 'permerror' or 'policy', but might return 'temperror'. The unused
         * values are just included for completeness and future development.
         * Regarding 'temperror', Mail::DKIM says: &quot;Returned if a
         * DKIM-Signature could not be checked due to some error which is
         * likely transient in nature, such as a temporary inability to
         * retrieve a public key. A later attempt may produce a better result.&quot;
         <p/>
         * Note that &quot;NONE&quot; won't be used in reference to a particular
         * signature, since it does not make sense to do so. It may be returned
         * from getSummaryResult(), however, or you may use it in your own code.
         <p/>
         @see <a href="http://www.dkim.org/specs/draft-kucherawy-sender-auth-header-14.html#rfc.section.2.4.1">http://www.dkim.org/specs/draft-kucherawy-sender-auth-header-14.html#rfc.section.2.4.1</a>
         */
        public static enum DkimStatus
        {
            PASS,
            NONE,
            INVALID,
            FAIL,
            TEMPERROR,
            PERMERROR,
            POLICY
        };

        /**
         * Called by BdkimAnalyzer code, this handles parsing of the BDKIM server response (the part after &quot;DS\n&quot;).
         * Not intended for public use.
         */
        protected static SignatureResults parse(String server_result)
        {
            SignatureResults sr = new SignatureResults();

            String lines[] = server_result.split("\n");

            if (lines == null || lines.length == 0return(null);

            try sr.num_sigs = Integer.parseInt(lines[0])}
            catch (NumberFormatException nfe) { return(null)}

            if (sr.num_sigs == 0return(sr);

            if (lines.length != sr.num_sigs * 1return(null);

            sr.results = new String[sr.num_sigs];
            sr.headerlists = new String[sr.num_sigs];
            sr.domains = new String[sr.num_sigs];

            for (int i = 1; i < lines.length; i+=3)
            {
                sr.domains[(i-1)/3= lines[i];
                sr.headerlists[(i-1)/3= lines[i+1];
                sr.results[(i-1)/3= lines[i+2];
            }

            return(sr);
        }

        /**
         * Returns the number of signtures represented in this SignatureResults object. You can use this
         * value to loop through the results. If this returns X>0, then you can call, for example,
         * getResult(0) through getResult(X-1) to get the results for the various signatures.
         @return the number of signtures represented in this SignatureResults object.
         */
        public int getNumberOfSignatures() { return(num_sigs)}

        /**
         * If you say: &quot;I just want one single pass/fail/none/invalid result for my message instead of an over-complex set of methods&quot;, then
         * this method may be for you.
         <p/>
         * Most verifiers will give you such a shorthand result (one of the main Mail::DKIM::Verifier methods, not used by BdkimAnalyzer, does so),
         * but they all make algorithmic decisions on how to handle cases where signatures disagree (e.g. what if one says pass,
         * one says fail, or if one says pass and one says invalid, or two say pass and one says fail, etc.) The best thing is for you to use
         * the various get() methods for the SignatureResults object and make your own algorithm which could, for example, take into account
         * which headers were signed. But if you just want &quot;one answer&quot; you can use this method.
         <p/>
         * Mail::DKIM's technique: &quot:In case of multiple signatures, the signature with the &quot;best&quot; result
         * will be returned. Best is defined as &quot;pass&quot;, followed by &quot;fail&quot;, &quot;invalid&quot;, and &quot;none&quot;&quot;.
         <p/>
         * Implemented here is the same algorithm as Mail::DKIM, with the added note that this ranking
         * goes as: pass, fail, policy, invalid, none, temperror, and permerror (but see documentation for getResult() about those results -- not
         * all are actually currently possible).
         <p/>
         * This will return from among the same group results at getResult() with the exception that this method can also return &quot;none&quot; if
         * the results object is empty (i.e. no signatures were present, whether valid or not).
         */
        public DkimStatus getSummaryResult()
        {
            if (num_sigs == 0return DkimStatus.NONE;
            if (num_sigs == 1return getResult(0);

            DkimStatus best_result = null;
            DkimStatus cur_result = null;

            for (int i=0; i<num_sigs; i++)
            {
                cur_result = getResult(i);

                if (cur_result == DkimStatus.PERMERROR &&
                        (best_result == null))
                    best_result = DkimStatus.PERMERROR;
                else if (cur_result == DkimStatus.TEMPERROR &&
                        (best_result == null || best_result == DkimStatus.PERMERROR))
                    best_result = DkimStatus.TEMPERROR;
                else if (cur_result == DkimStatus.NONE && // shouldn't ever happen, really, but just in case
                        (best_result == null || best_result == DkimStatus.PERMERROR || best_result == DkimStatus.TEMPERROR))
                    best_result = DkimStatus.NONE;
                else if (cur_result == DkimStatus.INVALID &&
                        (best_result == null || best_result == DkimStatus.PERMERROR || best_result == DkimStatus.TEMPERROR ||
                        best_result == DkimStatus.NONE))
                    best_result = DkimStatus.INVALID;
                else if (cur_result == DkimStatus.POLICY &&
                        (best_result != DkimStatus.PASS && best_result != DkimStatus.FAIL && best_result != DkimStatus.POLICY))
                    best_result = DkimStatus.POLICY;
                else if (cur_result == DkimStatus.FAIL &&
                        (best_result != DkimStatus.PASS && best_result != DkimStatus.FAIL))
                    best_result = DkimStatus.FAIL;
                else if (cur_result == DkimStatus.PASS && (best_result != DkimStatus.PASS))
                    best_result = DkimStatus.PASS;
            }

            if (best_result == nullbest_result = DkimStatus.NONE; // will never happen, but juuuuust to be safe.

            return(best_result);
        }

        /**
         * Returns the result of the evaluation of the signature at the given index in the results
         * arrays. Mail::DKIM returns more verbose information, but this method strips that out and gives one of
         * the following: &quot;pass&quot;, &quot;invalid&quot;, &quot;fail&quot;, or &quot;temperror&quot;. Note
         * that this method could technically return &quot;permerror&quot; or &quot;policy&quot; as well, but Mail::DKIM never
         * actually returns those values (as of version 0.39). Also note that since these are results for specific signatures,
         * you won't see &quot;none&quot;. A DomainKey/DKIM &quot;none&quot; result is indicated by getNumberOfSignatures() == 0.
         <p/>
         * This method will return null for bad values of index or other strangeness.
         <p/>
         * Regarding &quot;temperror&quot;, Mail::DKIM says: &quot;Returned if a DKIM-Signature could not be checked
         * due to some error which is likely transient in nature, such as a temporary
         * inability to retrieve a public key. A later attempt may produce a better result.&quot;
         <p/>
         @see <a href="http://www.dkim.org/specs/draft-kucherawy-sender-auth-header-14.html#rfc.section.2.4.1">http://www.dkim.org/specs/draft-kucherawy-sender-auth-header-14.html#rfc.section.2.4.1</a> for more info on DK/DKIM results.
         */
        public DkimStatus getResult(int index)
        {
            if (index < || index > num_sigs - 1return(null);

            if (results[index== null || results[index].equals("")) return(null);

            if (results[index].toLowerCase().startsWith("none"))
                return(DkimStatus.NONE);
            else if (results[index].toLowerCase().startsWith("pass"))
                return(DkimStatus.PASS);
            else if (results[index].toLowerCase().startsWith("invalid"))
                return(DkimStatus.INVALID);
            else if (results[index].toLowerCase().startsWith("fail"))
                return(DkimStatus.FAIL);
            else if (results[index].toLowerCase().startsWith("temperror"))
                return(DkimStatus.TEMPERROR);
            else if (results[index].toLowerCase().startsWith("permerror"))
                return(DkimStatus.PERMERROR);
            else if (results[index].toLowerCase().startsWith("policy"))
                return(DkimStatus.POLICY);
            else return(null);
        }

        /**
         * Returns the raw result string from Mail::DKIM.
         <p/>
         * Mail::DKIM returns results for each signature with some verbosity. The getResult() method strips that information out,
         * but this method will return the raw string that Mail::DKIM returned. This may be useful if, for example, you get a
         * DkimStatus.FAIL or INVALID or TEMPERROR and want to do something with the explanation string that Mail::DKIM returned,
         * such as log it or present it to a person as part of an explanation of why a message was denied, etc.
         <p/>
         * This method will return null for bad values of index or other strangeness.
         */
        public String getRawResult(int index)
        {
            if (index < || index > num_sigs - 1return(null);

            if (results[index== null || results[index].equals("")) return(null);

            return(results[index]);
        }

        /**
         * Useful for translating the DkimStatus values into readable strings.
         
         @return "none", "pass", "invalid", etc.
         */
        public static String resultToString(DkimStatus result)
        {
            switch(result)
            {
            case NONE: return("none");
            case PASS: return("pass");
            case INVALID: return("invalid");
            case FAIL: return("fail");
            case TEMPERROR: return("temperror");
            case PERMERROR: return("permerror");
            case POLICY: return("policy");
            defaultreturn("unrecognized_status");
            }
        }

        /**
         * This method returns the list of signed headers for the signature specified by the
         * index argument as an array of string values containing no whitespace or colons.
         <p/>
         * DomainKey and DKIM signatures specify which of the message headers
         * are signed by the signature -- this method lets you know which ones were signed.
         <p/>
         * This method returns null on errors.
         <p/>
         * Note that a Mail::DKIM limitation means that DomainKey-Signature headers that lack the
         * optional h= tag will return no header list at all even though in that case all headers
         * are signed (that tag is optional for DK, but not for DKIM).
         <p/>
         * Note that there is a chance there will be some whitespace in a given header... I can't
         * confirm with total certainty that in all cases Mail::DKIM won't generate whitespace
         * (e.g. malformed signatures, etc), so if you care, run your own check (e.g. do a
         * trim() and replace whitespace with &quot;&quot;). There won't be any newlines or
         * carriage returns in a header returned by this method..
         */
        public String[] getHeaderList(int index)
        {
            if (index < || index > num_sigs - 1return(null);

            if (headerlists[index== null || headerlists[index].equals("")) return(null);

            return(headerlists[index].split(":"));
        }

        /**
         * Returns the raw string from Mail::DKIM representing the (colon-separated) list of headers that were
         * signed by the signature at the given index.
         <p/>
         * This method will return null for bad values of index or other strangeness.
         */
        public String getRawHeaderList(int index)
        {
            if (index < || index > num_sigs - 1return(null);

            if (headerlists[index== null || headerlists[index].equals("")) return(null);

            return(headerlists[index]);
        }

        /**
         * Return the domain purportedly responsible for the signature with no whitespace, etc.
         <p/>
         * This method will return null for bad values of index or other strangeness.
         */
        public String getDomain(int index)
        {
            if (index < || index > num_sigs - 1return(null);

            if (domains[index== null || domains[index].equals("")) return(null);

            return(domains[index]);
        }
        
        /**
         * Generates a sort-of-pretty one-line string version of the contents of this SignatureResults object.
         */
        public String toString()
        {
            if (num_sigs <= 0return("[no signatures]");
            
            StringBuffer sb = new StringBuffer(num_sigs*64);

            for (int i=0; i<num_sigs; i++)
            {
                sb.append("[SIG#" + i + ":" + resultToString(getResult(i)) ":" + getDomain(i":HEADERS_SIGNED:[" + headerlists[i"]]");
            }
            
            return(sb.toString());
        }
    }
}