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 "localhost"
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 "temperror" 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 * "true" for the boolean.
121 */
122 public SignatureResults determineDkimStatuses(String raw_message) throws 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 "permerror" 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 "check for headers" 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 "false" 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_headers) throws AuthVerificationException
153 {
154 if (raw_message == null) throw 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_result) throws 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_exit) throws 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 != null) throw 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 == null) throw 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 != null) in.close();
417 if (out != null) out.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 != 1 || ias[0] == null) return("init failed: Got weird host array for host [" + host + "].");
452
453 connection_address = ias[0];
454
455 if (connection_timeout < 0) return("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. "localhost" or "127.0.0.1" or "123.123.123.123"
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: "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."
541 * <p/>
542 * Note that "NONE" 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 "DS\n").
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 == 0) return(null);
570
571 try { sr.num_sigs = Integer.parseInt(lines[0]); }
572 catch (NumberFormatException nfe) { return(null); }
573
574 if (sr.num_sigs == 0) return(sr);
575
576 if (lines.length != sr.num_sigs * 3 + 1) return(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: "I just want one single pass/fail/none/invalid result for my message instead of an over-complex set of methods", 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 "one answer" you can use this method.
609 * <p/>
610 * Mail::DKIM's technique: ":In case of multiple signatures, the signature with the "best" result
611 * will be returned. Best is defined as "pass", followed by "fail", "invalid", and "none"".
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 "none" 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 == 0) return DkimStatus.NONE;
623 if (num_sigs == 1) return 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 == null) best_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: "pass", "invalid", "fail", or "temperror". Note
664 * that this method could technically return "permerror" or "policy" 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 "none". A DomainKey/DKIM "none" 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 "temperror", Mail::DKIM says: "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."
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 < 0 || index > num_sigs - 1) return(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 < 0 || index > num_sigs - 1) return(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 default: return("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 ""). 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 < 0 || index > num_sigs - 1) return(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 < 0 || index > num_sigs - 1) return(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 < 0 || index > num_sigs - 1) return(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 <= 0) return("[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 }
|