BdkimAnalyzer.java
001 /**
002  * BdkimAnalyzer.java
003  * Copyright (c) 2011, Casey Connor
004  * All rights reserved.
005  
006  * Redistribution and use in source and binary forms, with or without modification,
007  * are permitted provided that the following conditions are met:
008  *
009  *    - Redistributions of source code must retain the above copyright notice, this
010  *      list of conditions and the following disclaimer.
011  *    - Redistributions in binary form must reproduce the above copyright notice,
012  *      this list of conditions and the following disclaimer in the documentation
013  *      and/or other materials provided with the distribution.
014  *    - Neither the name of Lacinato nor the names of its contributors may be used to
015  *      endorse or promote products derived from this software without specific prior
016  *      written permission.
017  *
018  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
019  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
020  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
021  * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
022  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
023  * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
024  * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
025  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
026  * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027  
028  @author Casey Connor -- lacinato.com
029  @version 1.0
030  
031  * For more information, see http://lacinato.com/cm/software/emailrelated/bdkim
032  
033  * A Java client to the BDKIM perl server process, this class provides convenience methods
034  * for sending a message to the perl server and receiving the evaluated DomainKey and DKIM
035  * results. It does not handle signing of messages, only verification.
036  */
037 
038 package com.lacinato.tools.bdkim;
039 
040 import java.net.Socket;
041 import java.net.InetAddress;
042 import java.net.InetSocketAddress;
043 import java.net.SocketTimeoutException;
044 import java.net.UnknownHostException;
045 import java.io.IOException;
046 import java.io.OutputStream;
047 import java.io.BufferedReader;
048 import java.io.InputStreamReader;
049 import java.io.StringReader;
050 
051 /**
052  * A Java client to the BDKIM perl server process, this class provides convenience methods
053  * for sending a message to the perl server and receiving the evaluated DomainKey and DKIM
054  * results. It does not handle signing of messages, only verification.
055  <p/>
056  * See determineDkimStatuses() for basic usage.
057  <p/>
058  * For more information, see <a href="http://lacinato.com/cm/software/emailrelated/bdkim">http://lacinato.com/cm/software/emailrelated/bdkim</a>
059  <p/>
060  @author Casey Connor -- <a href="http://lacinato.com">lacinato.com</a>
061  @version 1.0
062  */
063 public class BdkimAnalyzer
064 {
065     /**
066      * Default 12300
067      */
068     public static final int DEFAULT_PORT = 12300;
069     /**
070      * Default &quot;localhost&quot;
071      */
072     public static final String DEFAULT_HOST = "localhost";
073     /**
074      * Default 15 seconds
075      */
076     public static final int DEFAULT_CONNECTION_TIMEOUT = 15// seconds
077     /**
078      * Default 300 seconds - DNS calls could take a while on the perl side
079      */
080     public static final int DEFAULT_DATA_TIMEOUT = 300// seconds
081     /**
082      * Default 3 total attempts to connect to non-responsive server port
083      */
084     public static final int DEFAULT_CONNECT_TRIES = 3;
085 
086     private int port = DEFAULT_PORT;
087     private String host = DEFAULT_HOST;
088     private int connection_timeout = DEFAULT_CONNECTION_TIMEOUT * 1000;
089     private int data_timeout = DEFAULT_DATA_TIMEOUT * 1000;
090     private int connect_tries = DEFAULT_CONNECT_TRIES;
091     private InetAddress connection_address = null;
092 
093     private boolean init_complete = false;
094 
095     // lower case, include :
096     private static final String DKIM_HEADER_STRING = "dkim-signature:";
097     private static final String DK_HEADER_STRING = "domainkey-signature:";
098 
099     /**
100      * This is the main API call into BdkimAnalyzer.
101      <p/>
102      * Given a raw message (headers and all), this method will return the DK/DKIM results from the perl BDKIM server.
103      * It will throw an AuthVerificationException (with explanatory copy inside it) on errors, timeouts,
104      * etc. (Note that DKIM results of &quot;temperror&quot; et al. will not throw exceptions, but will return
105      * as results in the SignatureResults object.) See the SignatureResults documentation for
106      * important information about interpreting results.
107      <p/>
108      * Line endings are important in evaluating the DK/DKIM results of email messages.
109      * Message lines are supposed to end with \r\n. This method will make sure the lines have those
110      * endings before sending to the perl server, so you don't have to worry about it.
111      <p/>
112      * This class will do character encoding and decoding based on whatever the system character set is.
113      * It is a good idea that it use the same character set as your perl-side BDKIM server or the
114      * verification may have trouble.
115      <p/>
116      * On first use, this class will ensure that it can resolve the name of the server it will be connecting
117      * to and throw an AuthVerificationException if it can not.
118      <p/>
119      * This version of determineDkimStatuses will reflect to determineDkimStatuses(String, boolean) with
120      * &quot;true&quot; for the boolean.
121      */
122     public SignatureResults determineDkimStatuses(String raw_messagethrows AuthVerificationException
123     return(determineDkimStatuses(raw_message, true))}
124 
125     /**
126      * Given a raw message (headers and all), this method will return the DK/DKIM results from the perl BDKIM server.
127      * It will throw an AuthVerificationException (with explanatory copy inside it) on errors, timeouts,
128      * etc. (Note that DKIM &quot;permerror&quot; et al. will not throw exceptions, but will return
129      * as results in the SignatureResults object.) See the SignatureResults documentation for
130      * important information about interpreting results.
131      <p/>
132      * If &quot;check for headers&quot; is true, then this method will check the headers of the email
133      * message for DomainKey and DKIM signatures. If it finds none, it will not bother to send the
134      * messasge to the BDKIM server and will return an empty SignatureResults object. Use this feature
135      * if you are verifying messages that may not have such headers in them. If you know that the messages
136      * you are verifying have signatures in them, then you can pass this as &quot;false&quot; to save a
137      * tiny bit of wasted effort. Note that whatever the setting of this boolean, the BDKIM perl server
138      * does its own check of the headers before bothering to compute the results of the entire message
139      * (see private method queryBDKIM() for more on that).
140      <p/>
141      * Line endings are important in evaluating the DK/DKIM results of email messages.
142      * Message lines are supposed to end with \r\n. This method will make sure the lines have those
143      * endings before sending to the perl server, so you don't have to worry about it.
144      <p/>
145      * This class will do character encoding and decoding based on whatever the system character set is.
146      * It is a good idea that it use the same character set as your perl-side BDKIM server or the
147      * verification may have trouble.
148      <p/>
149      * On first use, this class will ensure that it can resolve the name of the server it will be connecting
150      * to and throw an AuthVerificationException if it can not.
151      */
152     public SignatureResults determineDkimStatuses(String raw_message, boolean check_for_headersthrows AuthVerificationException
153     {
154         if (raw_message == nullthrow new AuthVerificationException("determineDkimStatuses failed. Null message passed in.");
155 
156         String server_result = null;
157 
158         // if there are no headers, then there is no need to actually send to Mail::DKIM for the results
159         if (check_for_headers && (!hasDkOrDkimHeaders(raw_message))) return(new SignatureResults());
160         else
161         {
162             // DK or DKIM headers exist, or we were told not to check, so we need to hit the server.
163             // true == use early-exit if no sigs exist or if they are malformed... only partially redundant with hasDkOrDkimHeaders()
164             server_result = queryBdkim(raw_message, true);
165 
166             if (server_result == null || server_result.equals(""))
167                 throw new AuthVerificationException("determineDkimStatuses failed. Strange [result] from server: [" + server_result + "]");
168         }
169 
170         return(interpretResults(server_result));
171     }
172 
173     /**
174      * This method is public as a possible convenience only. You don't need it, generally.
175      
176      @return true if the raw email message represented by message_txt contains any DomainKey or DKIM headers in the header section (case-insensitive)
177      */
178     public boolean hasDkOrDkimHeaders(String message_txt)
179     {
180         BufferedReader my_bufr = new BufferedReader(new StringReader(message_txt));
181         String cur_header_line = null;
182 
183         while (true)
184         {
185             try cur_header_line = my_bufr.readLine()}
186             catch (IOException ioe) { return(false)// would be weird
187 
188             if (cur_header_line != null)
189             {
190                 cur_header_line = cur_header_line.toLowerCase();
191                 if (cur_header_line.startsWith(DKIM_HEADER_STRING|| cur_header_line.startsWith(DK_HEADER_STRING)) return(true);
192 
193                 if (cur_header_line.equals("")) return(false)// got to header/body separator without finding a relevant header
194             }
195             else return(false)// strange, no message body found... oh well.
196         }
197     }
198 
199     /**
200      * Given the response from the perl server, interpret the results and construct the SignatureResults object.
201      */
202     private SignatureResults interpretResults(String server_resultthrows AuthVerificationException
203     {
204         SignatureResults sig_res = null;
205 
206         if (server_result.startsWith("TE\n"))
207             throw new AuthVerificationException("interpretResults failed. BDKIM reports error transmitting message: " +
208                     server_result.substring(3, server_result.length() 1));
209         else if (server_result.startsWith("TS\nDE\n"))
210             throw new AuthVerificationException("interpretResults failed. BDKIM could not perform DKIM check: " +
211                     server_result.substring(6, server_result.length() 1));
212         else if (! server_result.startsWith("TS\nDS\n"))
213             throw new AuthVerificationException("interpretResults failed. Strange results from BDKIM: [" +
214                     server_result + "]");
215 
216         sig_res = SignatureResults.parse(server_result.substring(6, server_result.length() 1));
217 
218         if (sig_res == null)
219             throw new AuthVerificationException("interpretResults failed. Strange results from BDKIM server, could not parse: [" +
220                     server_result + "]");
221         
222         return(sig_res);
223     }
224 
225     /**
226      * Given the raw message text, send it to the BDKIM perl server and return what
227      * comes back.
228      
229      * The boolean "use_post_headers_dkim_early_exit" determines whether or not
230      * this method will send the appropriate protocol code to the BDKIM server telling it
231      * that we are interested to send the headers first and then see if any early results
232      * are possible. This is an efficiency tactic, since there may not be any signatures
233      * present (if we haven't checked on the client side) or if the signatures themselves
234      * are malformed. It is recommended that you pass in "true"
235      
236      * A couple things to note:
237      
238      * If use_post_headers_dkim_early_exit is true, then this method will clean up all line
239      * endings in the message to "\r\n", which is the proper way in RFC-2822 and what the perl
240      * server expects by default. If use_post_headers_dkim_early_exit is false,
241      * it will pass in the message_text as it was sent in, so note that distinction.
242      
243      * This method will do character encoding and decoding based on whatever the system character set is.
244      * It is a good idea that it use the same character set as your perl-side BDKIM server or the
245      * verification may have trouble.
246      */
247     private String queryBdkim(String message_text, boolean use_post_headers_dkim_early_exitthrows AuthVerificationException
248     {
249         // init this class if needed
250         if (! init_complete)
251         {
252             String s = init()// this method is synchronized for reentrant use
253             if (s != nullthrow new AuthVerificationException("queryBdkim failed. Could not init(): " + s);
254         }
255 
256         // connect to the server
257 
258         InetSocketAddress isa;
259         Socket socket = new Socket();
260 
261         try
262         {
263             isa = new InetSocketAddress(connection_address, port);
264         }
265         catch (IllegalArgumentException iae)
266         {
267             throw new AuthVerificationException("queryBdkim failed. Could not create socket addr object " +
268                                                 "with connection address: " + connection_address + " on port: " + port);
269         }
270 
271         int try_count = 1;
272 
273         while (try_count <= connect_tries)
274         {
275             try
276             {
277                 socket.connect(isa, getConnectionTimeoutMillis());
278             }
279             catch (SocketTimeoutException ste)
280             {
281                 // it'd be nice to log this fact
282                 try_count++;
283                 continue;
284             }
285             catch (IOException ioe)
286             {
287                 throw new AuthVerificationException("queryBdkim failed. Could not connect socket, address: " + connection_address + " on port: " +
288                                                     port + "; got ioe: " + ioe);
289             }
290             catch (Exception e)
291             {
292                 throw new AuthVerificationException("queryBdkim failed. Could not connect socket, address: " + connection_address + " on port: " +
293                                                     port + "; got strange exception: " + e);
294             }
295 
296             // Ok, got a good socket connect, so break out:
297             break;
298         }
299 
300         if (! socket.isConnected())
301         {
302             try
303             {
304                 socket.close();
305 
306                 throw new AuthVerificationException("queryBdkim failed. Could not connect socket after " +
307                                                     connect_tries + " attempts, address: " + connection_address + " on port: " +
308                                                     port);
309             }
310             catch (IOException ioe)
311             {
312                 throw new AuthVerificationException("queryBdkim failed. Could not close socket after this error: Could not connect socket after " +
313                                                     connect_tries + " attempts, address: " + connection_address + " on port: " + port);
314             }
315         }
316 
317         // send query, retrieve result
318 
319         OutputStream out = null;
320         BufferedReader in = null;
321         StringBuffer response = new StringBuffer(4096);
322 
323         try
324         {
325             // NOTE! This will do encoding/decoding based on the system charset!
326 
327             socket.setSoTimeout(getDataTimeoutMillis());
328 
329             out = socket.getOutputStream();
330             in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
331 
332             String s;
333 
334             if (use_post_headers_dkim_early_exit)
335             {
336                 // this branch of the method will fix all line endings
337 
338                 // write the EE code
339                 out.write("EE\r\n".getBytes());
340 
341                 // ...then write just the header part of the message
342                 BufferedReader my_bufr = new BufferedReader(new StringReader(message_text));
343 
344                 String cur_header_line = null;
345                 boolean keep_going = true;
346 
347                 while (keep_going)
348                 {
349                     cur_header_line = my_bufr.readLine();
350                     if (cur_header_line != null)
351                     {
352                         out.write((cur_header_line + "\r\n").getBytes());
353 
354                         if (cur_header_line.equals(""))
355                         {
356                             // found header/body separator
357                             out.flush();
358                             s = in.readLine();
359                             if (s == nullthrow new AuthVerificationException("queryBdkim failed. " +
360                                         "Could not get server response after headers sent in early exit mode. Got null.");
361                             else if (s.equals("HC"))
362                             {
363                                 // server wants the rest of the message
364                                 // note that we don't append "HC" to the "response" String from the server, for simplified parsing of results
365                                 while ((s = my_bufr.readLine()) != null)
366                                 {
367                                     out.write((s + "\r\n").getBytes());
368                                 }
369                             }
370                             else
371                             {
372                                 // server has (or should have) the results for us; assuming so, add this first line ("TS" or "TE")
373                                 // to the response String, we'll read in the rest when we fall through
374                                 response.append(s).append("\n");
375                                 keep_going = false;
376                             }
377                         }
378                     }
379                     else
380                     {
381                         keep_going = false// strange, no message body found... oh well.
382                     }
383                 }
384                 out.flush();
385                 my_bufr.close();
386             }
387             else
388             {
389                 // write the entire message
390                 // this branch of the method will not fix line endings!
391                 out.write(message_text.getBytes());
392                 out.flush();
393             }
394 
395             socket.shutdownOutput();
396 
397             // read in the server results (or, more precisely, the rest of them, in the EE case)
398 
399             while ((s = in.readLine()) != null)
400             {
401                 response.append(s).append("\n");
402             }
403         }
404         catch (IllegalStateException ise)
405         {
406             throw new AuthVerificationException("queryBdkim failed. Could not get server response. Got ise: " + ise);
407         }
408         catch (IOException ioe)
409         {
410             throw new AuthVerificationException("queryBdkim failed. Could not get server response. Got iao: " + ioe);
411         }
412         finally
413         {
414             try
415             {
416                 if (in != nullin.close();
417                 if (out != nullout.close();
418                 socket.close();
419             }
420             catch(IOException ioe)
421             {
422                 throw new AuthVerificationException("queryBdkim failed. Could not close one of in, out, or socket. Got ioe: " + ioe);
423             }
424         }
425 
426         return(response.toString());
427     }
428 
429     /**
430      * Set up the client for use. Will make sure the host name resolves to an IP. Called automatically when needed.
431      *
432      @return non-null string on any errors
433      */
434     private String init()
435     {
436         InetAddress[] ias = null;
437 
438         try
439         {
440             ias = InetAddress.getAllByName(host);
441         }
442         catch (UnknownHostException uhe)
443         {
444             return("init failed: Couldn't resolve supplied host of [" + host + "], got uhe: " + uhe);
445         }
446         catch (SecurityException se)
447         {
448             return("init failed: Couldn't resolve supplied host of [" + host + "], got se: " + se);
449         }
450 
451         if (ias.length != || ias[0== nullreturn("init failed: Got weird host array for host [" + host + "].");
452 
453         connection_address = ias[0];
454 
455         if (connection_timeout < 0return("init failed: connection_timeout must be > 0, it was: " + connection_timeout);
456 
457         init_complete = true;
458         return(null);
459     }
460 
461     /**
462      * Port to use when contacting the BDKIM perl serevr -- must be between 0 and 65535, inclusive
463      */
464     public void setPort(int p) { port = p; init_complete = false}
465     public int getPort() { return(port)}
466 
467     /**
468      * host name, or IP address in String form, pointing to the BDKIM perl server,
469      * e.g. &quot;localhost&quot; or &quot;127.0.0.1&quot; or &quot;123.123.123.123&quot;
470      */
471     public void setHost(String h) { host = h; init_complete = false}
472     public String getHost() { return(host)}
473 
474     /**
475      * Set the connection timeout value for connection to the BDKIM perl server, value
476      * specified in seconds; this only applies when first creating the connection, not
477      * during data transmission.
478      */
479     public void setConnectionTimeout(int s) { connection_timeout = s * 1000}
480     /**
481      @return the timeout value, in seconds
482      */
483     public int getConnectionTimeout() { return(connection_timeout/1000)}
484     private int getConnectionTimeoutMillis() { return(connection_timeout)}
485 
486     /**
487      * Set the connection timeout value for connection to the BDKIM perl server, value
488      * specified in seconds; this only applies when sending data to the server, not
489      * when making the original connection.
490      */
491     public void setDataTimeout(int s) { data_timeout = s * 1000}
492     /**
493      @return the timeout value, in seconds
494      */
495     public int getDataTimeout() { return(data_timeout/1000)}
496     private int getDataTimeoutMillis() { return(data_timeout)}
497 
498     /**
499      * Set the number of attempts to be made to connect to the server before
500      * failing a given connection attempt (for one message)
501      */
502     public void setConnectTries(int t) { connect_tries = t; }
503     public int getConnectTries() { return(connect_tries)}
504 
505     /**
506      * A structured data object that holds the results of the DomainKey/DKIM evaluation.
507      <p/>
508      * A count of the number of signatures encountered is kept. Using getNumberOfSignatures(),
509      * you can iterate through the results using the other access methods. The results are
510      * indexed starting at 0.
511      <p/>
512      * Any signature, whether valid or not, will get an entry in this results object. As long
513      * as the header name is present, it will be evaluated and a result will be found here.
514      */
515     public static class SignatureResults
516     {
517         private int num_sigs = 0;
518         private String results[] null;
519         private String headerlists[] null;
520         private String domains[] null;
521 
522         /**
523          * Creates an empty set of results (number of signaures == 0)
524          */
525         public SignatureResults()
526         {
527             num_sigs = 0;
528             results = null;
529             headerlists = null;
530             domains = null;
531         }
532 
533         /**
534          * DomainKey and DKIM results. Note that Mail::DKIM will not return
535          * 'permerror' or 'policy', but might return 'temperror'. The unused
536          * values are just included for completeness and future development.
537          * Regarding 'temperror', Mail::DKIM says: &quot;Returned if a
538          * DKIM-Signature could not be checked due to some error which is
539          * likely transient in nature, such as a temporary inability to
540          * retrieve a public key. A later attempt may produce a better result.&quot;
541          <p/>
542          * Note that &quot;NONE&quot; won't be used in reference to a particular
543          * signature, since it does not make sense to do so. It may be returned
544          * from getSummaryResult(), however, or you may use it in your own code.
545          <p/>
546          @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>
547          */
548         public static enum DkimStatus
549         {
550             PASS,
551             NONE,
552             INVALID,
553             FAIL,
554             TEMPERROR,
555             PERMERROR,
556             POLICY
557         };
558 
559         /**
560          * Called by BdkimAnalyzer code, this handles parsing of the BDKIM server response (the part after &quot;DS\n&quot;).
561          * Not intended for public use.
562          */
563         protected static SignatureResults parse(String server_result)
564         {
565             SignatureResults sr = new SignatureResults();
566 
567             String lines[] = server_result.split("\n");
568 
569             if (lines == null || lines.length == 0return(null);
570 
571             try sr.num_sigs = Integer.parseInt(lines[0])}
572             catch (NumberFormatException nfe) { return(null)}
573 
574             if (sr.num_sigs == 0return(sr);
575 
576             if (lines.length != sr.num_sigs * 1return(null);
577 
578             sr.results = new String[sr.num_sigs];
579             sr.headerlists = new String[sr.num_sigs];
580             sr.domains = new String[sr.num_sigs];
581 
582             for (int i = 1; i < lines.length; i+=3)
583             {
584                 sr.domains[(i-1)/3= lines[i];
585                 sr.headerlists[(i-1)/3= lines[i+1];
586                 sr.results[(i-1)/3= lines[i+2];
587             }
588 
589             return(sr);
590         }
591 
592         /**
593          * Returns the number of signtures represented in this SignatureResults object. You can use this
594          * value to loop through the results. If this returns X>0, then you can call, for example,
595          * getResult(0) through getResult(X-1) to get the results for the various signatures.
596          @return the number of signtures represented in this SignatureResults object.
597          */
598         public int getNumberOfSignatures() { return(num_sigs)}
599 
600         /**
601          * 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
602          * this method may be for you.
603          <p/>
604          * Most verifiers will give you such a shorthand result (one of the main Mail::DKIM::Verifier methods, not used by BdkimAnalyzer, does so),
605          * but they all make algorithmic decisions on how to handle cases where signatures disagree (e.g. what if one says pass,
606          * 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
607          * the various get() methods for the SignatureResults object and make your own algorithm which could, for example, take into account
608          * which headers were signed. But if you just want &quot;one answer&quot; you can use this method.
609          <p/>
610          * Mail::DKIM's technique: &quot:In case of multiple signatures, the signature with the &quot;best&quot; result
611          * will be returned. Best is defined as &quot;pass&quot;, followed by &quot;fail&quot;, &quot;invalid&quot;, and &quot;none&quot;&quot;.
612          <p/>
613          * Implemented here is the same algorithm as Mail::DKIM, with the added note that this ranking
614          * goes as: pass, fail, policy, invalid, none, temperror, and permerror (but see documentation for getResult() about those results -- not
615          * all are actually currently possible).
616          <p/>
617          * This will return from among the same group results at getResult() with the exception that this method can also return &quot;none&quot; if
618          * the results object is empty (i.e. no signatures were present, whether valid or not).
619          */
620         public DkimStatus getSummaryResult()
621         {
622             if (num_sigs == 0return DkimStatus.NONE;
623             if (num_sigs == 1return getResult(0);
624 
625             DkimStatus best_result = null;
626             DkimStatus cur_result = null;
627 
628             for (int i=0; i<num_sigs; i++)
629             {
630                 cur_result = getResult(i);
631 
632                 if (cur_result == DkimStatus.PERMERROR &&
633                         (best_result == null))
634                     best_result = DkimStatus.PERMERROR;
635                 else if (cur_result == DkimStatus.TEMPERROR &&
636                         (best_result == null || best_result == DkimStatus.PERMERROR))
637                     best_result = DkimStatus.TEMPERROR;
638                 else if (cur_result == DkimStatus.NONE && // shouldn't ever happen, really, but just in case
639                         (best_result == null || best_result == DkimStatus.PERMERROR || best_result == DkimStatus.TEMPERROR))
640                     best_result = DkimStatus.NONE;
641                 else if (cur_result == DkimStatus.INVALID &&
642                         (best_result == null || best_result == DkimStatus.PERMERROR || best_result == DkimStatus.TEMPERROR ||
643                         best_result == DkimStatus.NONE))
644                     best_result = DkimStatus.INVALID;
645                 else if (cur_result == DkimStatus.POLICY &&
646                         (best_result != DkimStatus.PASS && best_result != DkimStatus.FAIL && best_result != DkimStatus.POLICY))
647                     best_result = DkimStatus.POLICY;
648                 else if (cur_result == DkimStatus.FAIL &&
649                         (best_result != DkimStatus.PASS && best_result != DkimStatus.FAIL))
650                     best_result = DkimStatus.FAIL;
651                 else if (cur_result == DkimStatus.PASS && (best_result != DkimStatus.PASS))
652                     best_result = DkimStatus.PASS;
653             }
654 
655             if (best_result == nullbest_result = DkimStatus.NONE; // will never happen, but juuuuust to be safe.
656 
657             return(best_result);
658         }
659 
660         /**
661          * Returns the result of the evaluation of the signature at the given index in the results
662          * arrays. Mail::DKIM returns more verbose information, but this method strips that out and gives one of
663          * the following: &quot;pass&quot;, &quot;invalid&quot;, &quot;fail&quot;, or &quot;temperror&quot;. Note
664          * that this method could technically return &quot;permerror&quot; or &quot;policy&quot; as well, but Mail::DKIM never
665          * actually returns those values (as of version 0.39). Also note that since these are results for specific signatures,
666          * you won't see &quot;none&quot;. A DomainKey/DKIM &quot;none&quot; result is indicated by getNumberOfSignatures() == 0.
667          <p/>
668          * This method will return null for bad values of index or other strangeness.
669          <p/>
670          * Regarding &quot;temperror&quot;, Mail::DKIM says: &quot;Returned if a DKIM-Signature could not be checked
671          * due to some error which is likely transient in nature, such as a temporary
672          * inability to retrieve a public key. A later attempt may produce a better result.&quot;
673          <p/>
674          @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.
675          */
676         public DkimStatus getResult(int index)
677         {
678             if (index < || index > num_sigs - 1return(null);
679 
680             if (results[index== null || results[index].equals("")) return(null);
681 
682             if (results[index].toLowerCase().startsWith("none"))
683                 return(DkimStatus.NONE);
684             else if (results[index].toLowerCase().startsWith("pass"))
685                 return(DkimStatus.PASS);
686             else if (results[index].toLowerCase().startsWith("invalid"))
687                 return(DkimStatus.INVALID);
688             else if (results[index].toLowerCase().startsWith("fail"))
689                 return(DkimStatus.FAIL);
690             else if (results[index].toLowerCase().startsWith("temperror"))
691                 return(DkimStatus.TEMPERROR);
692             else if (results[index].toLowerCase().startsWith("permerror"))
693                 return(DkimStatus.PERMERROR);
694             else if (results[index].toLowerCase().startsWith("policy"))
695                 return(DkimStatus.POLICY);
696             else return(null);
697         }
698 
699         /**
700          * Returns the raw result string from Mail::DKIM.
701          <p/>
702          * Mail::DKIM returns results for each signature with some verbosity. The getResult() method strips that information out,
703          * but this method will return the raw string that Mail::DKIM returned. This may be useful if, for example, you get a
704          * DkimStatus.FAIL or INVALID or TEMPERROR and want to do something with the explanation string that Mail::DKIM returned,
705          * such as log it or present it to a person as part of an explanation of why a message was denied, etc.
706          <p/>
707          * This method will return null for bad values of index or other strangeness.
708          */
709         public String getRawResult(int index)
710         {
711             if (index < || index > num_sigs - 1return(null);
712 
713             if (results[index== null || results[index].equals("")) return(null);
714 
715             return(results[index]);
716         }
717 
718         /**
719          * Useful for translating the DkimStatus values into readable strings.
720          
721          @return "none", "pass", "invalid", etc.
722          */
723         public static String resultToString(DkimStatus result)
724         {
725             switch(result)
726             {
727             case NONE: return("none");
728             case PASS: return("pass");
729             case INVALID: return("invalid");
730             case FAIL: return("fail");
731             case TEMPERROR: return("temperror");
732             case PERMERROR: return("permerror");
733             case POLICY: return("policy");
734             defaultreturn("unrecognized_status");
735             }
736         }
737 
738         /**
739          * This method returns the list of signed headers for the signature specified by the
740          * index argument as an array of string values containing no whitespace or colons.
741          <p/>
742          * DomainKey and DKIM signatures specify which of the message headers
743          * are signed by the signature -- this method lets you know which ones were signed.
744          <p/>
745          * This method returns null on errors.
746          <p/>
747          * Note that a Mail::DKIM limitation means that DomainKey-Signature headers that lack the
748          * optional h= tag will return no header list at all even though in that case all headers
749          * are signed (that tag is optional for DK, but not for DKIM).
750          <p/>
751          * Note that there is a chance there will be some whitespace in a given header... I can't
752          * confirm with total certainty that in all cases Mail::DKIM won't generate whitespace
753          * (e.g. malformed signatures, etc), so if you care, run your own check (e.g. do a
754          * trim() and replace whitespace with &quot;&quot;). There won't be any newlines or
755          * carriage returns in a header returned by this method..
756          */
757         public String[] getHeaderList(int index)
758         {
759             if (index < || index > num_sigs - 1return(null);
760 
761             if (headerlists[index== null || headerlists[index].equals("")) return(null);
762 
763             return(headerlists[index].split(":"));
764         }
765 
766         /**
767          * Returns the raw string from Mail::DKIM representing the (colon-separated) list of headers that were
768          * signed by the signature at the given index.
769          <p/>
770          * This method will return null for bad values of index or other strangeness.
771          */
772         public String getRawHeaderList(int index)
773         {
774             if (index < || index > num_sigs - 1return(null);
775 
776             if (headerlists[index== null || headerlists[index].equals("")) return(null);
777 
778             return(headerlists[index]);
779         }
780 
781         /**
782          * Return the domain purportedly responsible for the signature with no whitespace, etc.
783          <p/>
784          * This method will return null for bad values of index or other strangeness.
785          */
786         public String getDomain(int index)
787         {
788             if (index < || index > num_sigs - 1return(null);
789 
790             if (domains[index== null || domains[index].equals("")) return(null);
791 
792             return(domains[index]);
793         }
794         
795         /**
796          * Generates a sort-of-pretty one-line string version of the contents of this SignatureResults object.
797          */
798         public String toString()
799         {
800             if (num_sigs <= 0return("[no signatures]");
801             
802             StringBuffer sb = new StringBuffer(num_sigs*64);
803 
804             for (int i=0; i<num_sigs; i++)
805             {
806                 sb.append("[SIG#" + i + ":" + resultToString(getResult(i)) ":" + getDomain(i":HEADERS_SIGNED:[" + headerlists[i"]]");
807             }
808             
809             return(sb.toString());
810         }
811     }
812 }