/**
* 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 "localhost"
*/
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 "temperror" 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
* "true" for the boolean.
*/
public SignatureResults determineDkimStatuses(String raw_message) throws 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 "permerror" 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 "check for headers" 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 "false" 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_headers) throws AuthVerificationException
{
if (raw_message == null) throw 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_result) throws 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_exit) throws AuthVerificationException
{
// init this class if needed
if (! init_complete)
{
String s = init(); // this method is synchronized for reentrant use
if (s != null) throw 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 == null) throw 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 != null) in.close();
if (out != null) out.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 != 1 || ias[0] == null) return("init failed: Got weird host array for host [" + host + "].");
connection_address = ias[0];
if (connection_timeout < 0) return("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. "localhost" or "127.0.0.1" or "123.123.123.123"
*/
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: "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."
* <p/>
* Note that "NONE" 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 "DS\n").
* 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 == 0) return(null);
try { sr.num_sigs = Integer.parseInt(lines[0]); }
catch (NumberFormatException nfe) { return(null); }
if (sr.num_sigs == 0) return(sr);
if (lines.length != sr.num_sigs * 3 + 1) return(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: "I just want one single pass/fail/none/invalid result for my message instead of an over-complex set of methods", 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 "one answer" you can use this method.
* <p/>
* Mail::DKIM's technique: ":In case of multiple signatures, the signature with the "best" result
* will be returned. Best is defined as "pass", followed by "fail", "invalid", and "none"".
* <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 "none" if
* the results object is empty (i.e. no signatures were present, whether valid or not).
*/
public DkimStatus getSummaryResult()
{
if (num_sigs == 0) return DkimStatus.NONE;
if (num_sigs == 1) return 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 == null) best_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: "pass", "invalid", "fail", or "temperror". Note
* that this method could technically return "permerror" or "policy" 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 "none". A DomainKey/DKIM "none" result is indicated by getNumberOfSignatures() == 0.
* <p/>
* This method will return null for bad values of index or other strangeness.
* <p/>
* Regarding "temperror", Mail::DKIM says: "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."
* <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 < 0 || index > num_sigs - 1) return(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 < 0 || index > num_sigs - 1) return(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");
default: return("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 ""). There won't be any newlines or
* carriage returns in a header returned by this method..
*/
public String[] getHeaderList(int index)
{
if (index < 0 || index > num_sigs - 1) return(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 < 0 || index > num_sigs - 1) return(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 < 0 || index > num_sigs - 1) return(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 <= 0) return("[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());
}
}
}
|