# coding: utf-8 # Copyright © 2011-2013 Julian Mehnle , # Copyright © 2011-2018 Scott Kitterman # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Module for parsing ``Authentication-Results`` headers as defined in RFC 5451, 7001, and 7601. """ #MODULE = 'authres' __author__ = 'Julian Mehnle, Scott Kitterman' __email__ = 'julian@mehnle.net' import re # Helper functions ############################################################################### retype = type(re.compile('')) def isre(obj): return isinstance(obj, retype) # Patterns ############################################################################### RFC2045_TOKEN_PATTERN = r"[A-Za-z0-9!#$%&'*+.^_`{|}~-]+" # Printable ASCII w/o tspecials RFC5234_WSP_PATTERN = r'[\t ]' RFC5234_VCHAR_PATTERN = r'[\x21-\x7e]' # Printable ASCII RFC5322_QUOTED_PAIR_PATTERN = r'\\[\t \x21-\x7e]' RFC5322_FWS_PATTERN = r'(?:%s*(?:\r\n|\n))?%s+' % (RFC5234_WSP_PATTERN, RFC5234_WSP_PATTERN) RFC5322_CTEXT_PATTERN = r'[\x21-\x27\x2a-\x5b\x5d-\x7e]' # Printable ASCII w/o ()\ RFC5322_ATEXT_PATTERN = r"[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]" # Printable ASCII w/o specials RFC5322_QTEXT_PATTERN = r'[\x21\x23-\x5b\x5d-\x7e]' # Printable ASCII w/o "\ KTEXT_PATTERN = r"[A-Za-z0-9!#$%&'*+?^_`{|}~-]" # Like atext, w/o /= PTEXT_PATTERN = r"[A-Za-z0-9!#$%&'*+/=?^_`{|}~.@-]" # Exceptions ############################################################################### class AuthResError(Exception): "Generic exception generated by the `authres` package" def __init__(self, message = None): Exception.__init__(self, message) self.message = message class SyntaxError(AuthResError): "Syntax error while parsing ``Authentication-Results`` header" def __init__(self, message = None, parse_text = None): AuthResError.__init__(self, message) if parse_text is None or len(parse_text) <= 40: self.parse_text = parse_text else: self.parse_text = parse_text[0:40] + '...' def __str__(self): if self.message and self.parse_text: return 'Syntax error: {0} at: {1}'.format(self.message, self.parse_text) elif self.message: return 'Syntax error: {0}'.format(self.message) elif self.parse_text: return 'Syntax error at: {0}'.format(self.parse_text) else: return 'Syntax error' class UnsupportedVersionError(AuthResError): "Unsupported ``Authentication-Results`` header version" def __init__(self, message = None, version = None): message = message or \ 'Unsupported Authentication-Results header version: %s' % version AuthResError.__init__(self, message) self.version = version class OrphanCommentError(AuthResError): "Comment without associated header element" # Main classes ############################################################################### # QuotableValue class # ============================================================================= class QuotableValue(str): """ An RFC 5451 ``value``/``pvalue`` with the capability to quote itself as an RFC 5322 ``quoted-string`` if necessary. """ def quote_if_needed(self): if re.search(r'@', self): return self elif re.match(r'^%s$' % RFC2045_TOKEN_PATTERN, self): return self else: return '"%s"' % re.sub(r'(["\\])', r'\\\1', self) # Escape "\ # AuthenticationResultProperty class # ============================================================================= class AuthenticationResultProperty(object): """ A property (``type.name=value``) of a result clause of an ``Authentication-Results`` header """ def __init__(self, type, name, value = None, comment = None): self.type = type.lower() self.name = name.lower() self.value = value and QuotableValue(value) self.comment = comment def __str__(self): if self.comment: return '%s.%s=%s (%s)' % (self.type, self.name, self.value.quote_if_needed(), self.comment) else: return '%s.%s=%s' % (self.type, self.name, self.value.quote_if_needed()) # Clarification of identifier naming: # The following function acts as a factory for Python property attributes to # be bound to a class, so it is named `make_result_class_properties`. Its # nested `getter` and `setter` functions use the identifier `result_property` # to refer to an instance of the `AuthenticationResultProperty` class. def make_result_class_properties(type, name): """ Return a property attribute to be bound to an `AuthenticationResult` class for accessing the `AuthenticationResultProperty` objects in its `properties` attribute. """ def value_getter(self, type = type, name = name): result_property = self._find_first_property(type, name) return result_property and result_property.value def comment_getter(self, type = type, name = name): result_property = self._find_first_property(type, name) return result_property and result_property.comment def value_setter(self, value, type = type, name = name): result_property = self._find_first_property(type, name) if not result_property: result_property = AuthenticationResultProperty(type, name) self.properties.append(result_property) result_property.value = value and QuotableValue(value) def comment_setter(self, comment, type = type, name = name): result_property = self._find_first_property(type, name) if not result_property: raise OrphanCommentError( "Cannot include result property comment without associated result property: %s.%s" % (type, name)) result_property.comment = comment return property(value_getter, value_setter), property(comment_getter, comment_setter) # AuthenticationResult and related classes # ============================================================================= class BaseAuthenticationResult(object): pass class NoneAuthenticationResult(BaseAuthenticationResult): "Sole ``none`` clause of an empty ``Authentication-Results`` header" def __init__(self, comment = None): self.comment = comment def __str__(self): if self.comment: return 'none (%s)' % self.comment else: return 'none' class AuthenticationResult(BaseAuthenticationResult): "Generic result clause of an ``Authentication-Results`` header" def __init__(self, method, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None ): self.method = method.lower() self.version = version and version.lower() self.result = result.lower() if not self.result: raise ValueError('Required result argument missing or None or empty') self.result_comment = result_comment self.reason = reason and QuotableValue(re.sub(r'[^\x20-\x7e]', '?', reason)) # Remove unprintable characters self.reason_comment = reason_comment self.properties = properties or [] def __str__(self): strs = [] strs.append(self.method) if self.version: strs.append('/') strs.append(self.version) strs.append('=') strs.append(self.result) if self.result_comment: strs.append(' (%s)' % self.result_comment) if self.reason: strs.append(' reason=%s' % self.reason.quote_if_needed()) if self.reason_comment: strs.append(' (%s)' % self.reason_comment) for property_ in self.properties: strs.append(' ') strs.append(str(property_)) return ''.join(strs) def _find_first_property(self, type, name): properties = [ property for property in self.properties if property.type == type and property.name == name ] return properties[0] if properties else None class DKIMAuthenticationResult(AuthenticationResult): "DKIM result clause of an ``Authentication-Results`` header" METHOD = 'dkim' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, header_d = None, header_d_comment = None, header_i = None, header_i_comment = None, header_a = None, header_a_comment = None, header_s = None, header_s_comment = None ): AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if header_d: self.header_d = header_d if header_d_comment: self.header_d_comment = header_d_comment if header_i: self.header_i = header_i if header_i_comment: self.header_i_comment = header_i_comment if header_a: self.header_a = header_a if header_a_comment: self.header_a_comment = header_a_comment if header_s: self.header_s = header_s if header_s_comment: self.header_s_comment = header_s_comment header_d, header_d_comment = make_result_class_properties('header', 'd') header_i, header_i_comment = make_result_class_properties('header', 'i') header_a, header_a_comment = make_result_class_properties('header', 'a') header_s, header_s_comment = make_result_class_properties('header', 's') def match_signature(self, signature_d): """Match authentication result against a DKIM signature by ``header.d``.""" return self.header_d == signature_d def match_signature_algorithm(self, signature_d, signature_a): """Match authentication result against a DKIM signature by ``header.d`` and ``header.a``.""" return self.header_d == signature_d and self.header_a == signature_a class DomainKeysAuthenticationResult(AuthenticationResult): "DomainKeys result clause of an ``Authentication-Results`` header" METHOD = 'domainkeys' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, header_d = None, header_d_comment = None, header_from = None, header_from_comment = None, header_sender = None, header_sender_comment = None ): AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if header_d: self.header_d = header_d if header_d_comment: self.header_d_comment = header_d_comment if header_from: self.header_from = header_from if header_from_comment: self.header_from_comment = header_from_comment if header_sender: self.header_sender = header_sender if header_sender_comment: self.header_sender_comment = header_sender_comment header_d, header_d_comment = make_result_class_properties('header', 'd') header_from, header_from_comment = make_result_class_properties('header', 'from') header_sender, header_sender_comment = make_result_class_properties('header', 'sender') def match_signature(self, signature_d): """Match authentication result against a DomainKeys signature by ``header.d``.""" return self.header_d == signature_d class SPFAuthenticationResult(AuthenticationResult): "SPF result clause of an ``Authentication-Results`` header" METHOD = 'spf' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, smtp_helo = None, smtp_helo_comment = None, smtp_mailfrom = None, smtp_mailfrom_comment = None ): AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if smtp_helo: self.smtp_helo = smtp_helo if smtp_helo_comment: self.smtp_helo_comment = smtp_helo_comment if smtp_mailfrom: self.smtp_mailfrom = smtp_mailfrom if smtp_mailfrom_comment: self.smtp_mailfrom_comment = smtp_mailfrom_comment smtp_helo, smtp_helo_comment = make_result_class_properties('smtp', 'helo') smtp_mailfrom, smtp_mailfrom_comment = make_result_class_properties('smtp', 'mailfrom') class SenderIDAuthenticationResult(AuthenticationResult): "Sender ID result clause of an ``Authentication-Results`` header" METHOD = 'sender-id' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, header_from = None, header_from_comment = None, header_sender = None, header_sender_comment = None, header_resent_from = None, header_resent_from_comment = None, header_resent_sender = None, header_resent_sender_comment = None ): AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if header_from: self.header_from = header_from if header_from_comment: self.header_from_comment = header_from_comment if header_sender: self.header_sender = header_sender if header_sender_comment: self.header_sender_comment = header_sender_comment if header_resent_from: self.header_resent_from = header_resent_from if header_resent_from_comment: self.header_resent_from_comment = header_resent_from_comment if header_resent_sender: self.header_resent_sender = header_resent_sender if header_resent_sender_comment: self.header_resent_sender_comment = header_resent_sender_comment header_from, header_from_comment = make_result_class_properties('header', 'from') header_sender, header_sender_comment = make_result_class_properties('header', 'sender') header_resent_from, header_resent_from_comment = make_result_class_properties('header', 'resent-from') header_resent_sender, header_resent_sender_comment = make_result_class_properties('header', 'resent-sender') @property def header_pra(self): return ( self.header_resent_sender or self.header_resent_from or self.header_sender or self.header_from ) @property def header_pra_comment(self): if self.header_resent_sender: return self.header_resent_sender_comment elif self.header_resent_from: return self.header_resent_from_comment elif self.header_sender: return self.header_sender_comment elif self.header_from: return self.header_from_comment else: return None class IPRevAuthenticationResult(AuthenticationResult): "iprev result clause of an ``Authentication-Results`` header" METHOD = 'iprev' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, policy_iprev = None, policy_iprev_comment = None ): AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if policy_iprev: self.policy_iprev = policy_iprev if policy_iprev_comment: self.policy_iprev_comment = policy_iprev_comment policy_iprev, policy_iprev_comment = make_result_class_properties('policy', 'iprev') class SMTPAUTHAuthenticationResult(AuthenticationResult): "SMTP AUTH result clause of an ``Authentication-Results`` header" METHOD = 'auth' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, # Added in RFC 7601, SMTP Auth method can refer to either the identity # confirmed in the auth command or the identity in auth parameter of # the SMTP Mail command, so we cover either option. smtp_auth = None, smtp_auth_comment = None, smtp_mailfrom = None, smtp_mailfrom_comment = None, ): AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if smtp_auth: self.smtp_auth = smtp_auth if smtp_auth_comment: self.smtp_auth_comment = smtp_auth_comment if smtp_mailfrom: self.smtp_mailfrom = smtp_mailfrom if smtp_mailfrom_comment: self.smtp_mailfrom_comment = smtp_mailfrom_comment smtp_mailfrom, smtp_mailfrom_comment = make_result_class_properties('smtp', 'mailfrom') smtp_auth, smtp_auth_comment = make_result_class_properties('smtp', 'auth') # AuthenticationResultsHeader class # ============================================================================= class AuthenticationResultsHeader(object): VERSIONS = ['1'] NONE_RESULT = NoneAuthenticationResult() HEADER_FIELD_NAME = 'Authentication-Results' HEADER_FIELD_PATTERN = re.compile(r'^Authentication-Results:\s*', re.I) @classmethod def parse(self, feature_context, string): """ Creates an `AuthenticationResultsHeader` object by parsing an ``Authentication- Results`` header (expecting the field name at the beginning). Expects the header to have been unfolded. """ string, n = self.HEADER_FIELD_PATTERN.subn('', string, 1) if n == 1: return self.parse_value(feature_context, string) else: raise SyntaxError('parse_with_name', 'Not an "Authentication-Results" header field: {0}'.format(string)) @classmethod def parse_value(self, feature_context, string): """ Creates an `AuthenticationResultsHeader` object by parsing an ``Authentication- Results`` header value. Expects the header value to have been unfolded. """ header = self(feature_context) header._parse_text = string.rstrip('\r\n\t ') header._parse() return header def __init__(self, feature_context, authserv_id = None, authserv_id_comment = None, version = None, version_comment = None, results = None, strict = False ): self.feature_context = feature_context self.authserv_id = authserv_id and authserv_id.lower() self.authserv_id_comment = authserv_id_comment self.version = version and str(version).lower() if self.version and not self.version in self.VERSIONS: raise UnsupportedVersionError(version = self.version) self.version_comment = version_comment if self.version_comment and not self.version: raise OrphanCommentError('Cannot include header version comment without associated header version') self.results = results or [] self.strict = strict # TODO Figure out how to set this programmatically def __str__(self): return ''.join((self.HEADER_FIELD_NAME, ': ', self.header_value())) def header_value(self): "Return just the value of Authentication-Results header." strs = [] strs.append(self.authserv_id) if self.authserv_id_comment: strs.append(' (%s)' % self.authserv_id_comment) if self.version: strs.append(' ') strs.append(self.version) if self.version_comment: strs.append(' (%s)' % self.version_comment) if len(self.results): for result in self.results: strs.append('; ') strs.append(str(result)) else: strs.append('; ') strs.append(str(self.NONE_RESULT)) return ''.join(strs) # Principal parser methods # ========================================================================= def _parse(self): authserv_id = self._parse_authserv_id() if not authserv_id: raise SyntaxError('Expected authserv-id', self._parse_text) self._parse_rfc5322_cfws() version = self._parse_version() if version and not version in self.VERSIONS: raise UnsupportedVersionError(version = version) self._parse_rfc5322_cfws() results = [] result = True while result: result = self._parse_resinfo() if result: results.append(result) if result == self.NONE_RESULT: break if not len(results): raise SyntaxError('Expected "none" or at least one resinfo', self._parse_text) elif results == [self.NONE_RESULT]: results = [] self._parse_rfc5322_cfws() self._parse_end() self.authserv_id = authserv_id.lower() self.version = version and version.lower() self.results = results def _parse_authserv_id(self): return self._parse_rfc5322_dot_atom() def _parse_version(self): version_match = self._parse_pattern(r'\d+') self._parse_rfc5322_cfws() return version_match and version_match.group() def _parse_resinfo(self): self._parse_rfc5322_cfws() if not self._parse_pattern(r';'): return self._parse_rfc5322_cfws() if self._parse_pattern(r'none'): return self.NONE_RESULT else: method, version, result = self._parse_methodspec() self._parse_rfc5322_cfws() reason = self._parse_reasonspec() properties = [] property_ = True while property_: try: self._parse_rfc5322_cfws() property_ = self._parse_propspec() if property_: properties.append(property_) except: if self.strict: raise else: pass return self.feature_context.result(method, version, result, None, reason, None, properties) def _parse_methodspec(self): self._parse_rfc5322_cfws() method, version = self._parse_method() self._parse_rfc5322_cfws() if not self._parse_pattern(r'='): raise SyntaxError('Expected "="', self._parse_text) self._parse_rfc5322_cfws() result = self._parse_rfc5322_dot_atom() if not result: raise SyntaxError('Expected result', self._parse_text) return (method, version, result) def _parse_method(self): method = self._parse_dot_key_atom() if not method: raise SyntaxError('Expected method', self._parse_text) self._parse_rfc5322_cfws() if not self._parse_pattern(r'/'): return (method, None) self._parse_rfc5322_cfws() version_match = self._parse_pattern(r'\d+') if not version_match: raise SyntaxError('Expected version', self._parse_text) return (method, version_match.group()) def _parse_reasonspec(self): if self._parse_pattern(r'reason'): self._parse_rfc5322_cfws() if not self._parse_pattern(r'='): raise SyntaxError('Expected "="', self._parse_text) self._parse_rfc5322_cfws() reasonspec = self._parse_rfc2045_value() if not reasonspec: raise SyntaxError('Expected reason', self._parse_text) return reasonspec def _parse_propspec(self): ptype = self._parse_key_atom() if not ptype: return elif ptype.lower() not in ['smtp', 'header', 'body', 'policy']: self._parse_rfc5322_cfws() self._parse_pattern(r'\.') self._parse_rfc5322_cfws() self._parse_dot_key_atom() self._parse_pattern(r'=') self._parse_pvalue() raise SyntaxError('Invalid ptype; expected any of "smtp", "header", "body", "policy", got "{0}"'.format(ptype)) self._parse_rfc5322_cfws() if not self._parse_pattern(r'\.'): raise SyntaxError('Expected "."', self._parse_text) self._parse_rfc5322_cfws() property_ = self._parse_dot_key_atom() self._parse_rfc5322_cfws() if not self._parse_pattern(r'='): raise SyntaxError('Expected "="', self._parse_text) pvalue = self._parse_pvalue() if pvalue is None: raise SyntaxError('Expected pvalue', self._parse_text) return AuthenticationResultProperty(ptype, property_, pvalue) def _parse_pvalue(self): self._parse_rfc5322_cfws() # The original rule is (modulo CFWS): # # pvalue = [ [local-part] "@" ] domain-name / value # value = token / quoted-string # # Distinguishing from may require backtracking, # and in order to avoid the need for that, the following is a simpli- # fication of the rule from RFC 5451, erring on the side of # laxity. # # Since is either a or , and # is either a or a , and and # are very similar ( is a superset of except # that multiple dots may not be adjacent), we allow a union of ".", # "@" and characters (jointly denoted ) in the place of # and . # # Furthermore we allow an empty string by requiring a sequence of zero # or more, rather than one or more (as required by RFC 2045's ), # characters. # # We then allow four patterns: # # pvalue = quoted-string / # quoted-string "@" domain-name / # "@" domain-name / # *ptext quoted_string = self._parse_rfc5322_quoted_string() if quoted_string: if self._parse_pattern(r'@'): # quoted-string "@" domain-name domain_name = self._parse_rfc5322_dot_atom() self._parse_rfc5322_cfws() if domain_name: return '"%s"@%s' % (quoted_string, domain_name) else: # quoted-string self._parse_rfc5322_cfws() # Look ahead to see whether pvalue terminates after quoted-string as expected: if re.match(r';|$', self._parse_text): return quoted_string else: if self._parse_pattern(r'@'): # "@" domain-name domain_name = self._parse_rfc5322_dot_atom() self._parse_rfc5322_cfws() if domain_name: return '@' + domain_name else: # *ptext pvalue_match = self._parse_pattern(r'%s*' % PTEXT_PATTERN) self._parse_rfc5322_cfws() if pvalue_match: return pvalue_match.group() def _parse_end(self): if self._parse_text == '': return True else: raise SyntaxError('Expected end of text', self._parse_text) # Generic grammar parser methods # ========================================================================= def _parse_pattern(self, pattern): match = [None] def matched(m): match[0] = m return '' # TODO: This effectively recompiles most patterns on each use, which # is far from efficient. This should be rearchitected. regexp = pattern if isre(pattern) else re.compile(r'^' + pattern, re.I) self._parse_text = regexp.sub(matched, self._parse_text, 1) return match[0] def _parse_rfc2045_value(self): return self._parse_rfc2045_token() or self._parse_rfc5322_quoted_string() def _parse_rfc2045_token(self): token_match = self._parse_pattern(RFC2045_TOKEN_PATTERN) return token_match and token_match.group() def _parse_rfc5322_quoted_string(self): self._parse_rfc5322_cfws() if not self._parse_pattern(r'^"'): return all_qcontent = '' qcontent = True while qcontent: fws_match = self._parse_pattern(RFC5322_FWS_PATTERN) if fws_match: all_qcontent += fws_match.group() qcontent = self._parse_rfc5322_qcontent() if qcontent: all_qcontent += qcontent self._parse_pattern(RFC5322_FWS_PATTERN) if not self._parse_pattern(r'"'): raise SyntaxError('Expected <">', self._parse_text) self._parse_rfc5322_cfws() return all_qcontent def _parse_rfc5322_qcontent(self): qtext_match = self._parse_pattern(r'%s+' % RFC5322_QTEXT_PATTERN) if qtext_match: return qtext_match.group() quoted_pair_match = self._parse_pattern(RFC5322_QUOTED_PAIR_PATTERN) if quoted_pair_match: return quoted_pair_match.group() def _parse_rfc5322_dot_atom(self): self._parse_rfc5322_cfws() dot_atom_text_match = self._parse_pattern(r'%s+(?:\.%s+)*' % (RFC5322_ATEXT_PATTERN, RFC5322_ATEXT_PATTERN)) self._parse_rfc5322_cfws() return dot_atom_text_match and dot_atom_text_match.group() def _parse_dot_key_atom(self): # Like _parse_rfc5322_dot_atom, but disallows "/" (forward slash) and # "=" (equal sign). self._parse_rfc5322_cfws() dot_atom_text_match = self._parse_pattern(r'%s+(?:\.%s+)*' % (KTEXT_PATTERN, KTEXT_PATTERN)) self._parse_rfc5322_cfws() return dot_atom_text_match and dot_atom_text_match.group() def _parse_key_atom(self): # Like _parse_dot_key_atom, but also disallows "." (dot). self._parse_rfc5322_cfws() dot_atom_text_match = self._parse_pattern(r'%s+' % KTEXT_PATTERN) self._parse_rfc5322_cfws() return dot_atom_text_match and dot_atom_text_match.group() def _parse_rfc5322_cfws(self): fws_match = False comment_match = True while comment_match: fws_match = fws_match or self._parse_pattern(RFC5322_FWS_PATTERN) comment_match = self._parse_rfc5322_comment() fws_match = fws_match or self._parse_pattern(RFC5322_FWS_PATTERN) return fws_match or comment_match def _parse_rfc5322_comment(self): if self._parse_pattern(r'\('): while self._parse_pattern(RFC5322_FWS_PATTERN) or self._parse_rfc5322_ccontent(): pass if self._parse_pattern(r'^\)'): return True else: raise SyntaxError('comment: expected FWS or ccontent or ")"', self._parse_text) def _parse_rfc5322_ccontent(self): if self._parse_pattern(r'%s+' % RFC5322_CTEXT_PATTERN): return True elif self._parse_pattern(RFC5322_QUOTED_PAIR_PATTERN): return True elif self._parse_rfc5322_comment(): return True # Authentication result classes directory ############################################################################### RESULT_CLASSES = [ DKIMAuthenticationResult, DomainKeysAuthenticationResult, SPFAuthenticationResult, SenderIDAuthenticationResult, IPRevAuthenticationResult, SMTPAUTHAuthenticationResult ] # vim:sw=4 sts=4