# coding: utf-8
# Copyright © 2011-2013 Julian Mehnle <julian@mehnle.net>,
# Copyright © 2011-2018 Scott Kitterman <scott@kitterman.com>
#
# 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 <token> from <domain-name> may require backtracking,
# and in order to avoid the need for that, the following is a simpli-
# fication of the <pvalue> rule from RFC 5451, erring on the side of
# laxity.
#
# Since <local-part> is either a <quoted-string> or <dot-atom>, and
# <value> is either a <quoted-string> or a <token>, and <dot-atom> and
# <token> are very similar (<dot-atom> is a superset of <token> except
# that multiple dots may not be adjacent), we allow a union of ".",
# "@" and <atext> characters (jointly denoted <ptext>) in the place of
# <dot-atom> and <token>.
#
# 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 <token>),
# <ptext> 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
|