3
# Copyright © 2011-2013 Julian Mehnle <julian@mehnle.net>,
4
# Copyright © 2011-2013 Scott Kitterman <scott@kitterman.com>
6
# Licensed under the Apache License, Version 2.0 (the "License");
7
# you may not use this file except in compliance with the License.
8
# You may obtain a copy of the License at
10
# http://www.apache.org/licenses/LICENSE-2.0
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS,
14
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
# See the License for the specific language governing permissions and
16
# limitations under the License.
19
Module for parsing ``Authentication-Results`` headers as defined in RFC 5451,
25
__author__ = 'Julian Mehnle, Scott Kitterman'
26
__email__ = 'julian@mehnle.net'
31
###############################################################################
33
retype = type(re.compile(''))
36
return isinstance(obj, retype)
39
###############################################################################
41
RFC2045_TOKEN_PATTERN = r"[A-Za-z0-9!#$%&'*+.^_`{|}~-]+" # Printable ASCII w/o tspecials
42
RFC5234_WSP_PATTERN = r'[\t ]'
43
RFC5234_VCHAR_PATTERN = r'[\x21-\x7e]' # Printable ASCII
44
RFC5322_QUOTED_PAIR_PATTERN = r'\\[\t \x21-\x7e]'
45
RFC5322_FWS_PATTERN = r'(?:%s*(?:\r\n|\n))?%s+' % (RFC5234_WSP_PATTERN, RFC5234_WSP_PATTERN)
46
RFC5322_CTEXT_PATTERN = r'[\x21-\x27\x2a-\x5b\x5d-\x7e]' # Printable ASCII w/o ()\
47
RFC5322_ATEXT_PATTERN = r"[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]" # Printable ASCII w/o specials
48
RFC5322_QTEXT_PATTERN = r'[\x21\x23-\x5b\x5d-\x7e]' # Printable ASCII w/o "\
49
KTEXT_PATTERN = r"[A-Za-z0-9!#$%&'*+?^_`{|}~-]" # Like atext, w/o /=
50
PTEXT_PATTERN = r"[A-Za-z0-9!#$%&'*+/=?^_`{|}~.@-]"
53
###############################################################################
55
class AuthResError(Exception):
56
"Generic exception generated by the `authres` package"
58
def __init__(self, message = None):
59
Exception.__init__(self, message)
60
self.message = message
62
class SyntaxError(AuthResError):
63
"Syntax error while parsing ``Authentication-Results`` header"
65
def __init__(self, message = None, parse_text = None):
66
AuthResError.__init__(self, message)
67
if parse_text is None or len(parse_text) <= 40:
68
self.parse_text = parse_text
70
self.parse_text = parse_text[0:40] + '...'
73
if self.message and self.parse_text:
74
return 'Syntax error: {0} at: {1}'.format(self.message, self.parse_text)
76
return 'Syntax error: {0}'.format(self.message)
78
return 'Syntax error at: {0}'.format(self.parse_text)
82
class UnsupportedVersionError(AuthResError):
83
"Unsupported ``Authentication-Results`` header version"
85
def __init__(self, message = None, version = None):
86
message = message or \
87
'Unsupported Authentication-Results header version: %s' % version
88
AuthResError.__init__(self, message)
89
self.version = version
91
class OrphanCommentError(AuthResError):
92
"Comment without associated header element"
95
###############################################################################
98
# =============================================================================
100
class QuotableValue(str):
102
An RFC 5451 ``value``/``pvalue`` with the capability to quote itself as an
103
RFC 5322 ``quoted-string`` if necessary.
105
def quote_if_needed(self):
106
if re.search(r'@', self):
108
elif re.match(r'^%s$' % RFC2045_TOKEN_PATTERN, self):
111
return '"%s"' % re.sub(r'(["\\])', r'\\\1', self) # Escape "\
113
# AuthenticationResultProperty class
114
# =============================================================================
116
class AuthenticationResultProperty(object):
118
A property (``type.name=value``) of a result clause of an
119
``Authentication-Results`` header
122
def __init__(self, type, name, value = None, comment = None):
123
self.type = type.lower()
124
self.name = name.lower()
125
self.value = value and QuotableValue(value)
126
self.comment = comment
130
return '%s.%s=%s (%s)' % (self.type, self.name, self.value.quote_if_needed(), self.comment)
132
return '%s.%s=%s' % (self.type, self.name, self.value.quote_if_needed())
134
# Clarification of identifier naming:
135
# The following function acts as a factory for Python property attributes to
136
# be bound to a class, so it is named `make_result_class_properties`. Its
137
# nested `getter` and `setter` functions use the identifier `result_property`
138
# to refer to an instance of the `AuthenticationResultProperty` class.
139
def make_result_class_properties(type, name):
141
Return a property attribute to be bound to an `AuthenticationResult` class
142
for accessing the `AuthenticationResultProperty` objects in its `properties`
146
def value_getter(self, type = type, name = name):
147
result_property = self._find_first_property(type, name)
148
return result_property and result_property.value
150
def comment_getter(self, type = type, name = name):
151
result_property = self._find_first_property(type, name)
152
return result_property and result_property.comment
154
def value_setter(self, value, type = type, name = name):
155
result_property = self._find_first_property(type, name)
156
if not result_property:
157
result_property = AuthenticationResultProperty(type, name)
158
self.properties.append(result_property)
159
result_property.value = value and QuotableValue(value)
161
def comment_setter(self, comment, type = type, name = name):
162
result_property = self._find_first_property(type, name)
163
if not result_property:
164
raise OrphanCommentError(
165
"Cannot include result property comment without associated result property: %s.%s" % (type, name))
166
result_property.comment = comment
168
return property(value_getter, value_setter), property(comment_getter, comment_setter)
170
# AuthenticationResult and related classes
171
# =============================================================================
173
class BaseAuthenticationResult(object): pass
175
class NoneAuthenticationResult(BaseAuthenticationResult):
176
"Sole ``none`` clause of an empty ``Authentication-Results`` header"
178
def __init__(self, comment = None):
179
self.comment = comment
183
return 'none (%s)' % self.comment
187
class AuthenticationResult(BaseAuthenticationResult):
188
"Generic result clause of an ``Authentication-Results`` header"
190
def __init__(self, method, version = None,
191
result = None, result_comment = None,
192
reason = None, reason_comment = None,
195
self.method = method.lower()
196
self.version = version and version.lower()
197
self.result = result.lower()
199
raise ValueError('Required result argument missing or None or empty')
200
self.result_comment = result_comment
201
self.reason = reason and QuotableValue(re.sub(r'[^\x20-\x7e]', '?', reason))
202
# Remove unprintable characters
203
self.reason_comment = reason_comment
204
self.properties = properties or []
208
strs.append(self.method)
211
strs.append(self.version)
213
strs.append(self.result)
214
if self.result_comment:
215
strs.append(' (%s)' % self.result_comment)
217
strs.append(' reason=%s' % self.reason.quote_if_needed())
218
if self.reason_comment:
219
strs.append(' (%s)' % self.reason_comment)
220
for property_ in self.properties:
222
strs.append(str(property_))
225
def _find_first_property(self, type, name):
230
if property.type == type and property.name == name
232
return properties[0] if properties else None
234
class DKIMAuthenticationResult(AuthenticationResult):
235
"DKIM result clause of an ``Authentication-Results`` header"
239
def __init__(self, version = None,
240
result = None, result_comment = None,
241
reason = None, reason_comment = None,
243
header_d = None, header_d_comment = None,
244
header_i = None, header_i_comment = None
246
AuthenticationResult.__init__(self, self.METHOD, version,
247
result, result_comment, reason, reason_comment, properties)
248
if header_d: self.header_d = header_d
249
if header_d_comment: self.header_d_comment = header_d_comment
250
if header_i: self.header_i = header_i
251
if header_i_comment: self.header_i_comment = header_i_comment
253
header_d, header_d_comment = make_result_class_properties('header', 'd')
254
header_i, header_i_comment = make_result_class_properties('header', 'i')
256
def match_signature(self, signature_d):
257
"""Match authentication result against a DKIM signature by ``header.d``."""
259
return self.header_d == signature_d
261
class DomainKeysAuthenticationResult(AuthenticationResult):
262
"DomainKeys result clause of an ``Authentication-Results`` header"
264
METHOD = 'domainkeys'
266
def __init__(self, version = None,
267
result = None, result_comment = None,
268
reason = None, reason_comment = None,
270
header_d = None, header_d_comment = None,
271
header_from = None, header_from_comment = None,
272
header_sender = None, header_sender_comment = None
274
AuthenticationResult.__init__(self, self.METHOD, version,
275
result, result_comment, reason, reason_comment, properties)
276
if header_d: self.header_d = header_d
277
if header_d_comment: self.header_d_comment = header_d_comment
278
if header_from: self.header_from = header_from
279
if header_from_comment: self.header_from_comment = header_from_comment
280
if header_sender: self.header_sender = header_sender
281
if header_sender_comment: self.header_sender_comment = header_sender_comment
283
header_d, header_d_comment = make_result_class_properties('header', 'd')
284
header_from, header_from_comment = make_result_class_properties('header', 'from')
285
header_sender, header_sender_comment = make_result_class_properties('header', 'sender')
287
def match_signature(self, signature_d):
288
"""Match authentication result against a DomainKeys signature by ``header.d``."""
290
return self.header_d == signature_d
292
class SPFAuthenticationResult(AuthenticationResult):
293
"SPF result clause of an ``Authentication-Results`` header"
297
def __init__(self, version = None,
298
result = None, result_comment = None,
299
reason = None, reason_comment = None,
301
smtp_helo = None, smtp_helo_comment = None,
302
smtp_mailfrom = None, smtp_mailfrom_comment = None
304
AuthenticationResult.__init__(self, self.METHOD, version,
305
result, result_comment, reason, reason_comment, properties)
306
if smtp_helo: self.smtp_helo = smtp_helo
307
if smtp_helo_comment: self.smtp_helo_comment = smtp_helo_comment
308
if smtp_mailfrom: self.smtp_mailfrom = smtp_mailfrom
309
if smtp_mailfrom_comment: self.smtp_mailfrom_comment = smtp_mailfrom_comment
311
smtp_helo, smtp_helo_comment = make_result_class_properties('smtp', 'helo')
312
smtp_mailfrom, smtp_mailfrom_comment = make_result_class_properties('smtp', 'mailfrom')
314
class SenderIDAuthenticationResult(AuthenticationResult):
315
"Sender ID result clause of an ``Authentication-Results`` header"
319
def __init__(self, version = None,
320
result = None, result_comment = None,
321
reason = None, reason_comment = None,
323
header_from = None, header_from_comment = None,
324
header_sender = None, header_sender_comment = None,
325
header_resent_from = None, header_resent_from_comment = None,
326
header_resent_sender = None, header_resent_sender_comment = None
328
AuthenticationResult.__init__(self, self.METHOD, version,
329
result, result_comment, reason, reason_comment, properties)
330
if header_from: self.header_from = header_from
331
if header_from_comment: self.header_from_comment = header_from_comment
332
if header_sender: self.header_sender = header_sender
333
if header_sender_comment: self.header_sender_comment = header_sender_comment
334
if header_resent_from: self.header_resent_from = header_resent_from
335
if header_resent_from_comment: self.header_resent_from_comment = header_resent_from_comment
336
if header_resent_sender: self.header_resent_sender = header_resent_sender
337
if header_resent_sender_comment: self.header_resent_sender_comment = header_resent_sender_comment
339
header_from, header_from_comment = make_result_class_properties('header', 'from')
340
header_sender, header_sender_comment = make_result_class_properties('header', 'sender')
341
header_resent_from, header_resent_from_comment = make_result_class_properties('header', 'resent-from')
342
header_resent_sender, header_resent_sender_comment = make_result_class_properties('header', 'resent-sender')
345
def header_pra(self):
347
self.header_resent_sender or
348
self.header_resent_from or
349
self.header_sender or
354
def header_pra_comment(self):
355
if self.header_resent_sender:
356
return self.header_resent_sender_comment
357
elif self.header_resent_from:
358
return self.header_resent_from_comment
359
elif self.header_sender:
360
return self.header_sender_comment
361
elif self.header_from:
362
return self.header_from_comment
366
class IPRevAuthenticationResult(AuthenticationResult):
367
"iprev result clause of an ``Authentication-Results`` header"
371
def __init__(self, version = None,
372
result = None, result_comment = None,
373
reason = None, reason_comment = None,
375
policy_iprev = None, policy_iprev_comment = None
377
AuthenticationResult.__init__(self, self.METHOD, version,
378
result, result_comment, reason, reason_comment, properties)
379
if policy_iprev: self.policy_iprev = policy_iprev
380
if policy_iprev_comment: self.policy_iprev_comment = policy_iprev_comment
382
policy_iprev, policy_iprev_comment = make_result_class_properties('policy', 'iprev')
384
class SMTPAUTHAuthenticationResult(AuthenticationResult):
385
"SMTP AUTH result clause of an ``Authentication-Results`` header"
389
def __init__(self, version = None,
390
result = None, result_comment = None,
391
reason = None, reason_comment = None,
393
# Added in RFC 7601, SMTP Auth method can refer to either the identity
394
# confirmed in the auth command or the identity in auth parameter of
395
# the SMTP Mail command, so we cover either option.
396
smtp_auth = None, smtp_auth_comment = None,
397
smtp_mailfrom = None, smtp_mailfrom_comment = None,
399
AuthenticationResult.__init__(self, self.METHOD, version,
400
result, result_comment, reason, reason_comment, properties)
401
if smtp_auth: self.smtp_auth = smtp_auth
402
if smtp_auth_comment: self.smtp_auth_comment = smtp_auth_comment
403
if smtp_mailfrom: self.smtp_mailfrom = smtp_mailfrom
404
if smtp_mailfrom_comment: self.smtp_mailfrom_comment = smtp_mailfrom_comment
406
smtp_mailfrom, smtp_mailfrom_comment = make_result_class_properties('smtp', 'mailfrom')
407
smtp_auth, smtp_auth_comment = make_result_class_properties('smtp', 'auth')
409
# AuthenticationResultsHeader class
410
# =============================================================================
412
class AuthenticationResultsHeader(object):
415
NONE_RESULT = NoneAuthenticationResult()
417
HEADER_FIELD_NAME = 'Authentication-Results'
418
HEADER_FIELD_PATTERN = re.compile(r'^Authentication-Results:\s*', re.I)
421
def parse(self, feature_context, string):
423
Creates an `AuthenticationResultsHeader` object by parsing an ``Authentication-
424
Results`` header (expecting the field name at the beginning). Expects the
425
header to have been unfolded.
427
string, n = self.HEADER_FIELD_PATTERN.subn('', string, 1)
429
return self.parse_value(feature_context, string)
431
raise SyntaxError('parse_with_name', 'Not an "Authentication-Results" header field: {0}'.format(string))
434
def parse_value(self, feature_context, string):
436
Creates an `AuthenticationResultsHeader` object by parsing an ``Authentication-
437
Results`` header value. Expects the header value to have been unfolded.
439
header = self(feature_context)
440
header._parse_text = string.rstrip('\r\n\t ')
446
authserv_id = None, authserv_id_comment = None,
447
version = None, version_comment = None,
450
self.feature_context = feature_context
451
self.authserv_id = authserv_id and authserv_id.lower()
452
self.authserv_id_comment = authserv_id_comment
453
self.version = version and str(version).lower()
454
if self.version and not self.version in self.VERSIONS:
455
raise UnsupportedVersionError(version = self.version)
456
self.version_comment = version_comment
457
if self.version_comment and not self.version:
458
raise OrphanCommentError('Cannot include header version comment without associated header version')
459
self.results = results or []
462
return ''.join((self.HEADER_FIELD_NAME, ': ', self.header_value()))
464
def header_value(self):
465
"Return just the value of Authentication-Results header."
467
strs.append(self.authserv_id)
468
if self.authserv_id_comment:
469
strs.append(' (%s)' % self.authserv_id_comment)
472
strs.append(self.version)
473
if self.version_comment:
474
strs.append(' (%s)' % self.version_comment)
475
if len(self.results):
476
for result in self.results:
478
strs.append(str(result))
481
strs.append(str(self.NONE_RESULT))
484
# Principal parser methods
485
# =========================================================================
488
authserv_id = self._parse_authserv_id()
490
raise SyntaxError('Expected authserv-id', self._parse_text)
492
self._parse_rfc5322_cfws()
494
version = self._parse_version()
495
if version and not version in self.VERSIONS:
496
raise UnsupportedVersionError(version = version)
498
self._parse_rfc5322_cfws()
503
result = self._parse_resinfo()
505
results.append(result)
506
if result == self.NONE_RESULT:
509
raise SyntaxError('Expected "none" or at least one resinfo', self._parse_text)
510
elif results == [self.NONE_RESULT]:
513
self._parse_rfc5322_cfws()
516
self.authserv_id = authserv_id.lower()
517
self.version = version and version.lower()
518
self.results = results
520
def _parse_authserv_id(self):
521
return self._parse_rfc5322_dot_atom()
523
def _parse_version(self):
524
version_match = self._parse_pattern(r'\d+')
525
self._parse_rfc5322_cfws()
526
return version_match and version_match.group()
528
def _parse_resinfo(self):
529
self._parse_rfc5322_cfws()
530
if not self._parse_pattern(r';'):
532
self._parse_rfc5322_cfws()
533
if self._parse_pattern(r'none'):
534
return self.NONE_RESULT
536
method, version, result = self._parse_methodspec()
537
self._parse_rfc5322_cfws()
538
reason = self._parse_reasonspec()
542
self._parse_rfc5322_cfws()
543
property_ = self._parse_propspec()
545
properties.append(property_)
546
return self.feature_context.result(method, version, result, None, reason, None, properties)
548
def _parse_methodspec(self):
549
self._parse_rfc5322_cfws()
550
method, version = self._parse_method()
551
self._parse_rfc5322_cfws()
552
if not self._parse_pattern(r'='):
553
raise SyntaxError('Expected "="', self._parse_text)
554
self._parse_rfc5322_cfws()
555
result = self._parse_rfc5322_dot_atom()
557
raise SyntaxError('Expected result', self._parse_text)
558
return (method, version, result)
560
def _parse_method(self):
561
method = self._parse_dot_key_atom()
563
raise SyntaxError('Expected method', self._parse_text)
564
self._parse_rfc5322_cfws()
565
if not self._parse_pattern(r'/'):
566
return (method, None)
567
self._parse_rfc5322_cfws()
568
version_match = self._parse_pattern(r'\d+')
569
if not version_match:
570
raise SyntaxError('Expected version', self._parse_text)
571
return (method, version_match.group())
573
def _parse_reasonspec(self):
574
if self._parse_pattern(r'reason'):
575
self._parse_rfc5322_cfws()
576
if not self._parse_pattern(r'='):
577
raise SyntaxError('Expected "="', self._parse_text)
578
self._parse_rfc5322_cfws()
579
reasonspec = self._parse_rfc2045_value()
581
raise SyntaxError('Expected reason', self._parse_text)
584
def _parse_propspec(self):
585
ptype = self._parse_key_atom()
588
elif ptype.lower() not in ['smtp', 'header', 'body', 'policy']:
589
raise SyntaxError('Invalid ptype; expected any of "smtp", "header", "body", "policy", got "%s"' % ptype, self._parse_text)
590
self._parse_rfc5322_cfws()
591
if not self._parse_pattern(r'\.'):
592
raise SyntaxError('Expected "."', self._parse_text)
593
self._parse_rfc5322_cfws()
594
property_ = self._parse_dot_key_atom()
595
self._parse_rfc5322_cfws()
596
if not self._parse_pattern(r'='):
597
raise SyntaxError('Expected "="', self._parse_text)
598
pvalue = self._parse_pvalue()
600
raise SyntaxError('Expected pvalue', self._parse_text)
601
return AuthenticationResultProperty(ptype, property_, pvalue)
603
def _parse_pvalue(self):
604
self._parse_rfc5322_cfws()
606
# The original rule is (modulo CFWS):
608
# pvalue = [ [local-part] "@" ] domain-name / value
609
# value = token / quoted-string
611
# Distinguishing <token> from <domain-name> may require backtracking,
612
# and in order to avoid the need for that, the following is a simpli-
613
# fication of the <pvalue> rule from RFC 5451, erring on the side of
616
# Since <local-part> is either a <quoted-string> or <dot-atom>, and
617
# <value> is either a <quoted-string> or a <token>, and <dot-atom> and
618
# <token> are very similar (<dot-atom> is a superset of <token> except
619
# that multiple dots may not be adjacent), we allow a union of ".",
620
# "@" and <atext> characters (jointly denoted <ptext>) in the place of
621
# <dot-atom> and <token>.
623
# Furthermore we allow an empty string by requiring a sequence of zero
624
# or more, rather than one or more (as required by RFC 2045's <token>),
625
# <ptext> characters.
627
# We then allow four patterns:
629
# pvalue = quoted-string /
630
# quoted-string "@" domain-name /
634
quoted_string = self._parse_rfc5322_quoted_string()
636
if self._parse_pattern(r'@'):
637
# quoted-string "@" domain-name
638
domain_name = self._parse_rfc5322_dot_atom()
639
self._parse_rfc5322_cfws()
641
return '"%s"@%s' % (quoted_string, domain_name)
644
self._parse_rfc5322_cfws()
645
# Look ahead to see whether pvalue terminates after quoted-string as expected:
646
if re.match(r';|$', self._parse_text):
649
if self._parse_pattern(r'@'):
651
domain_name = self._parse_rfc5322_dot_atom()
652
self._parse_rfc5322_cfws()
654
return '@' + domain_name
657
pvalue_match = self._parse_pattern(r'%s*' % PTEXT_PATTERN)
658
self._parse_rfc5322_cfws()
660
return pvalue_match.group()
662
def _parse_end(self):
663
if self._parse_text == '':
666
raise SyntaxError('Expected end of text', self._parse_text)
668
# Generic grammar parser methods
669
# =========================================================================
671
def _parse_pattern(self, pattern):
678
# TODO: This effectively recompiles most patterns on each use, which
679
# is far from efficient. This should be rearchitected.
680
regexp = pattern if isre(pattern) else re.compile(r'^' + pattern, re.I)
681
self._parse_text = regexp.sub(matched, self._parse_text, 1)
684
def _parse_rfc2045_value(self):
685
return self._parse_rfc2045_token() or self._parse_rfc5322_quoted_string()
687
def _parse_rfc2045_token(self):
688
token_match = self._parse_pattern(RFC2045_TOKEN_PATTERN)
689
return token_match and token_match.group()
691
def _parse_rfc5322_quoted_string(self):
692
self._parse_rfc5322_cfws()
693
if not self._parse_pattern(r'^"'):
698
fws_match = self._parse_pattern(RFC5322_FWS_PATTERN)
700
all_qcontent += fws_match.group()
701
qcontent = self._parse_rfc5322_qcontent()
703
all_qcontent += qcontent
704
self._parse_pattern(RFC5322_FWS_PATTERN)
705
if not self._parse_pattern(r'"'):
706
raise SyntaxError('Expected <">', self._parse_text)
707
self._parse_rfc5322_cfws()
710
def _parse_rfc5322_qcontent(self):
711
qtext_match = self._parse_pattern(r'%s+' % RFC5322_QTEXT_PATTERN)
713
return qtext_match.group()
714
quoted_pair_match = self._parse_pattern(RFC5322_QUOTED_PAIR_PATTERN)
715
if quoted_pair_match:
716
return quoted_pair_match.group()
718
def _parse_rfc5322_dot_atom(self):
719
self._parse_rfc5322_cfws()
720
dot_atom_text_match = self._parse_pattern(r'%s+(?:\.%s+)*' %
721
(RFC5322_ATEXT_PATTERN, RFC5322_ATEXT_PATTERN))
722
self._parse_rfc5322_cfws()
723
return dot_atom_text_match and dot_atom_text_match.group()
725
def _parse_dot_key_atom(self):
726
# Like _parse_rfc5322_dot_atom, but disallows "/" (forward slash) and
728
self._parse_rfc5322_cfws()
729
dot_atom_text_match = self._parse_pattern(r'%s+(?:\.%s+)*' %
730
(KTEXT_PATTERN, KTEXT_PATTERN))
731
self._parse_rfc5322_cfws()
732
return dot_atom_text_match and dot_atom_text_match.group()
734
def _parse_key_atom(self):
735
# Like _parse_dot_key_atom, but also disallows "." (dot).
736
self._parse_rfc5322_cfws()
737
dot_atom_text_match = self._parse_pattern(r'%s+' % KTEXT_PATTERN)
738
self._parse_rfc5322_cfws()
739
return dot_atom_text_match and dot_atom_text_match.group()
741
def _parse_rfc5322_cfws(self):
745
fws_match = fws_match or self._parse_pattern(RFC5322_FWS_PATTERN)
746
comment_match = self._parse_rfc5322_comment()
747
fws_match = fws_match or self._parse_pattern(RFC5322_FWS_PATTERN)
748
return fws_match or comment_match
750
def _parse_rfc5322_comment(self):
751
if self._parse_pattern(r'\('):
752
while self._parse_pattern(RFC5322_FWS_PATTERN) or self._parse_rfc5322_ccontent(): pass
753
if self._parse_pattern(r'^\)'):
756
raise SyntaxError('comment: expected FWS or ccontent or ")"', self._parse_text)
758
def _parse_rfc5322_ccontent(self):
759
if self._parse_pattern(r'%s+' % RFC5322_CTEXT_PATTERN):
761
elif self._parse_pattern(RFC5322_QUOTED_PAIR_PATTERN):
763
elif self._parse_rfc5322_comment():
766
# Authentication result classes directory
767
###############################################################################
770
DKIMAuthenticationResult,
771
DomainKeysAuthenticationResult,
772
SPFAuthenticationResult,
773
SenderIDAuthenticationResult,
774
IPRevAuthenticationResult,
775
SMTPAUTHAuthenticationResult