authres-1.2.0/0000755000175000017500000000000013521360303014643 5ustar kittermakitterma00000000000000authres-1.2.0/CHANGES0000644000175000017500000001367113521357147015662 0ustar kittermakitterma00000000000000# Legend: # --- = A new release # + = Added a feature (in a backwards compatible way) # ! = Changed something significant, or removed a feature # * = Fixed a bug, or made a minor improvement --- 1.2.0 2019-08-03 + Added ability to include DMARC policy in DMARC results * Updated references for new RFCs, ARC no longer experimental * Converted http references to https (thanks DKG) --- 1.1.1 2018-10-30 + Added ARC specific tags for draft-ietf-dmarc-arc-protocol-18 (as of IETF last call, still experimental), smtp.remote-ip and header.oldest-pass --- 1.1.0 2018-03-02 + Changed from distutils to setuptools because it's the future + Implement RFC 7601 SHOULD to ignore unknown method identifiers (2.7.6): + Discard unknown ptypes and associated properties + Added tests to document errors raised by different kinds of broken header fields --- 1.0.2 2018-02-16 + Added DKIM 'a' property so signature algorithm can be reported as proposed for inclusion in draft-ietf-dmarc-rfc7601bis (experimental) + Added match_signature_algorithm to the DKIMAuthenticationResult class to make it easier to find the correct DKIM result based on both domain and algorithm + Added DKIM 's' property so signature algorithm can be reported as proposed for inclusion in draft-ietf-dmarc-rfc7601bis (experimental) --- 1.0.1 (2017-08-04) + Extended experimental support for ARC results --- 1.0.0 (2017-07-14) + Added initial experimental support for ARC results + Switch to semantic versioning scheme and only set version in setup.py and __init__ --- 0.900 (2016-12-11) + Add support for RFC 7601 use of SMTP auth with the mailfrom property * Updated README and docstring reference to refer to RFC 7601 --- 0.800 (2015-03-09 22:40 -0400) + Add support for RFC 7293, The Require-Recipient-Valid-Since Header Field and SMTP Service Extension, header field types --- 0.702 (2015-01-25 19:01 -0500) * Adjust examples and add discussion in the README about reporting SPF HELO and Mail From results correctly --- 0.701 (2014-06-25 06:25 -0400) * Remove unintended artifacts from tarball --- 0.700 (2014-06-25 06:12 -0400) + Add support for RFC 7281, Authentication-Results Registration for S/MIME Signature Verification, header field types * Minor improvements to setup.py * Updated README and docstrings to refer to RFC 7001 in addition to RFC 5451 --- 0.602 (2013-07-29 18:39) * Tolerate empty , such as in "smtp.mailfrom=". RFC 5451, via RFC 2045, requires at least one character, but it seems more useful and only a minor violation of the spec to parse out an empty string rather than raising an exception. --- 0.601 (2013-04-27 04:23) * When stringifying RFC 5451 property values (pvalue), format them as quoted- strings if they contain spaces or special characters (and are not e-mail addresses). E.g., IPv6 addresses in policy.iprev properties must be double-quoted. * Fix broken references to quoted_string variable in authres.core. AuthenticationResultsHeader._parse_pvalue method. (Closes: LP #1165978) * Fix erroneous reference to ArgumentError exception to refer to ValueError instead. When does the Ruby compatibility layer for Python come out? * Added additional tests/examples in authres/tests --- 0.600 (2013-04-04 05:22) + Added support for DMARC as described in draft-kucherawy-dmarc-base-00 --- 0.501 (2013-02-11 20:17) * Make authres.dkim_b.DKIMAuthenticationResult inherit from authres.core. DKIMAuthenticationResult, not authres.core.AuthenticationResult. --- 0.500 (2013-02-05 20:25) + Add match_signature methods to DKIMAuthenticationResult and DomainKeys- AuthenticationResult classes implementing matching against a DKIM/DK signature by d=, and, in the case of the authres.dkim_b module, b= (per RFC 6008, section 4). --- 0.402 (2012-06-22 05:26) * Fix setup.py to install authres/tests (LP: #1007909) + Add AuthenticationResultsHeader.header_value() method. ! Moved doctest initiation from __init__.py to __main__.py so tests only run when the module is called as the main program --- 0.401 (2012-04-13 20:27) * Fix MANIFEST.in to include correct files --- 0.4 (2012-04-13 20:02) * Update note on use of fail instead of hardfail for SPF results now that the IANA registry has been updated and RFC 6577 published * Fix doctests to run when authres is imported ! No longer will run as main, which wasn't doing anything anyway * Split doctests and move most to authres/tests to make the docstrings bearable ! Changed pypi classifier for development status to production/stable --- 0.399 (2012-02-09 06:15) + Added support for comments on headers where no authentication is performed + Added support in DKIMAuthenticationResults class for RFC 6008 cryptographic identification header (header.b) + Added support for RFC 5617 DKIM ADSP authentication method in new DKIMADSPAuthenticationResult class + Added support for RFC 6212 Vouch By Reference (VBR) authentication method in new VBRAuthenticationResult class --- 0.3 (2012-01-07 05:24) + Added support for comments on most header elements to object constructors. Comments are correctly stringified from constructed objects, however conversely, are not currently parsed from strings. ! The authres function and most object constructor signatures have changed in terms of argument order to allow for new comment arguments. Using named arguments is recommended. * Added additional doctests from RFC 5451 examples --- 0.2 (2011-08-10 16:50) ! Reworked API from scratch to be more properly OO, extendable, and maintainable + Parse authentication results headers ! Started rework of doctests to match new API and be more readable + Specific subclasses for creating and parsing dkim, domainKeys, spf, senderid, iprev, and smtp auth authentication methods --- 0.1 (2011-03-14 18:00) + Initial release + Create authentication results headers authres-1.2.0/COPYING0000644000175000017500000002614013521356646015720 0ustar kittermakitterma00000000000000 Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. authres-1.2.0/MANIFEST.in0000644000175000017500000000015513365721774016425 0ustar kittermakitterma00000000000000include authres/*py include authres/tests include README include COPYING include CHANGES include MANIFEST.in authres-1.2.0/PKG-INFO0000644000175000017500000000171213521360303015741 0ustar kittermakitterma00000000000000Metadata-Version: 1.1 Name: authres Version: 1.2.0 Summary: authres - Authentication Results Header Module Home-page: https://launchpad.net/authentication-results-python Author: Julian Mehnle, Scott Kitterman Author-email: julian@mehnle.net License: Apache 2.0 Description: UNKNOWN Keywords: dkim,spf,dmarc,email,authentication,rfc5451,rfc7001,rfc7601,rfc8601 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Communications :: Email :: Mail Transport Agents Classifier: Topic :: Communications :: Email :: Filters Classifier: Topic :: Software Development :: Libraries :: Python Modules authres-1.2.0/README0000644000175000017500000000436513521356727015552 0ustar kittermakitterma00000000000000This module (authres) is designed to provide support for RFC 5451/7001, originally https://tools.ietf.org/html/rfc5451 (and then https://tools.ietf.org/html/rfc7001 and https://tools.ietf.org/html/rfc7601) and currently https://tools.ietf.org/html/rfc8601, processing in Python. It also supports Authentication Results extensions: RFC 5617 DKIM/ADSP RFC 6008 DKIM signature identification (header.b) RFC 6212 Vouch By Reference (VBR) RFC 6577 Sender Policy Framework (SPF) RFC 7281 Authentication-Results Registration for S/MIME RFC 7293, The Require-Recipient-Valid-Since Header Field and SMTP Service Extension, header field types RFC 7489 Domain-based Message Authentication, Reporting, and Conformance (DMARC) RFC 8617 The Authenticated Received Chain (ARC) Protocol Support marked experimental may change or be removed without consideration for backward compatibility. RFC 7410, A Property Types Registry for the Authentication-Results Header Field, added the possibility for new ptypes to be created, but so far none have been registered. import authres only provides the RFC 5451/7001/7601/8601 types. Individual additions can be imported by name, import authres.dkim_b, authres.dkim_adsp, authres.vbr, authres.dmarc, and authres.smime. To use all the features, the feature context authres.all_features() is provided. Header folding and unfolding is the responsibility of the calling application. Between docstrings and RFC 5451/7001/7601/8601 the API should be reasonably clear, but the docstrings are still a work in progress. Additional examples are available in the file authres/tests. SPF authentication results can relate to either the HELO identity or the Mail From. When creating header fields with SPF results, only set the identity used to evaluate the SPF result that's reported. You can use multiple objects to report both in a single header field. See the example in the tests file. This package requires python2.6 or later (including python3). It has been tested at different times with python2.6, python2.7, python3.2, python3.3, python3.4, python3.5, and python3.6. The current release was tested with python3.7.. To execute doctests, run the module as main: python3 -m authres This is completely untested on Windows, but in theory should work. authres-1.2.0/authres/0000755000175000017500000000000013521360303016316 5ustar kittermakitterma00000000000000authres-1.2.0/authres/__init__.py0000644000175000017500000002106313521356646020450 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2011-2013 Julian Mehnle , # Copyright © 2011-2013 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. """ Package for parsing ``Authentication-Results`` headers as defined in RFC 5451/7001/7601/8601. Optional support for authentication methods defined in RFCs 5617, 6008, 6212, 7281, and 8617. Examples: RFC 5451 B.2 >>> str(AuthenticationResultsHeader('test.example.org')) 'Authentication-Results: test.example.org; none' RFC 5451 B.3 >>> str(AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [SPFAuthenticationResult(result = 'pass', ... smtp_mailfrom = 'example.net')])) 'Authentication-Results: example.com; spf=pass smtp.mailfrom=example.net' RFC 5451 B.4(1) >>> str(AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [SMTPAUTHAuthenticationResult(result = 'pass', result_comment = 'cram-md5', ... smtp_auth = 'sender@example.net'), SPFAuthenticationResult(result = 'pass', ... smtp_mailfrom = 'example.net')])) 'Authentication-Results: example.com; auth=pass (cram-md5) smtp.auth=sender@example.net; spf=pass smtp.mailfrom=example.net' RFC 5451 B.4(2) >>> str(AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [SenderIDAuthenticationResult(result = 'pass', ... header_from = 'example.com')])) 'Authentication-Results: example.com; sender-id=pass header.from=example.com' RFC 5451 B.5(1) # Note: RFC 5451 uses 'hardfail' instead of 'fail' for SPF failures. Hardfail is deprecated. See RFC 6577. Examples here use the correct 'fail'. The authres module does not validate result codes, so either will be processed. >>> str(AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [SenderIDAuthenticationResult(result = 'fail', ... header_from = 'example.com'), DKIMAuthenticationResult(result = 'pass', ... header_i = 'sender@example.com', result_comment = 'good signature')])) 'Authentication-Results: example.com; sender-id=fail header.from=example.com; dkim=pass (good signature) header.i=sender@example.com' # Missing parsing header comment. #FIXME >>> arobj = AuthenticationResultsHeader.parse('Authentication-Results: example.com; sender-id=fail header.from=example.com; dkim=pass (good signature) header.i=sender@example.com') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'sender-id=fail header.from=example.com' >>> str(arobj.results[0].method) 'sender-id' >>> str(arobj.results[0].result) 'fail' >>> str(arobj.results[0].header_from) 'example.com' >>> str(arobj.results[0].properties[0].type) 'header' >>> str(arobj.results[0].properties[0].name) 'from' >>> str(arobj.results[0].properties[0].value) 'example.com' >>> str(arobj.results[1]) 'dkim=pass header.i=sender@example.com' >>> str(arobj.results[1].method) 'dkim' >>> str(arobj.results[1].result) 'pass' >>> str(arobj.results[1].header_i) 'sender@example.com' >>> str(arobj.results[1].properties[0].type) 'header' >>> str(arobj.results[1].properties[0].name) 'i' >>> str(arobj.results[1].properties[0].value) 'sender@example.com' """ MODULE = 'authres' __author__ = 'Julian Mehnle, Scott Kitterman' __email__ = 'julian@mehnle.net' __version__ = '1.2.0' import authres.core # Backward compatibility: For the benefit of user modules referring to authres.…: from authres.core import * # FeatureContext class & convenience methods ############################################################################### class FeatureContext(object): """ Class representing a "feature context" for the ``authres`` package. A feature context is a collection of extension modules that may override the core AuthenticationResultsHeader class or result classes, or provide additional result classes for new authentication methods. To instantiate a feature context, import the desired ``authres.…`` extension modules and pass them to ``FeatureContext()``. A ``FeatureContext`` object provides ``parse``, ``parse_value``, ``header``, and ``result`` methods specific to the context's feature set. """ def __init__(self, *modules): self.header_class = authres.core.AuthenticationResultsHeader self.result_class_by_auth_method = {} modules = [authres.core] + list(modules) for module in modules: try: self.header_class = module.AuthenticationResultsHeader except AttributeError: # Module does not provide new AuthenticationResultsHeader class. pass try: for result_class in module.RESULT_CLASSES: self.result_class_by_auth_method[result_class.METHOD] = result_class except AttributeError: # Module does not provide AuthenticationResult subclasses. pass def parse(self, string): return self.header_class.parse(self, string) def parse_value(self, string): return self.header_class.parse_value(self, string) def header(self, authserv_id = None, authserv_id_comment = None, version = None, version_comment = None, results = None ): return self.header_class( self, authserv_id, authserv_id_comment, version, version_comment, results) def result(self, method, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None ): try: return self.result_class_by_auth_method[method](version, result, result_comment, reason, reason_comment, properties) except KeyError: return authres.core.AuthenticationResult(method, version, result, result_comment, reason, reason_comment, properties) _core_features = None def core_features(): "Returns default feature context providing only RFC 5451 core features." global _core_features if not _core_features: _core_features = FeatureContext() return _core_features _all_features = None def all_features(): """ Returns default feature context providing all features shipped with the ``authres`` package. """ global _all_features if not _all_features: import authres.dkim_b import authres.dkim_adsp import authres.vbr import authres.dmarc import authres.smime import authres.rrvs import authres.arc _all_features = FeatureContext( authres.dkim_b, authres.dkim_adsp, authres.vbr, authres.dmarc, authres.smime, authres.rrvs, authres.arc ) return _all_features # Simple API with implicit core-features-only context ############################################################################### class AuthenticationResultsHeader(authres.core.AuthenticationResultsHeader): @classmethod def parse(self, string): return authres.core.AuthenticationResultsHeader.parse(core_features(), string) @classmethod def parse_value(self, string): return authres.core.AuthenticationResultsHeader.parse_value(core_features(), string) def __init__(self, authserv_id = None, authserv_id_comment = None, version = None, version_comment = None, results = None ): authres.core.AuthenticationResultsHeader.__init__(self, core_features(), authserv_id, authserv_id_comment, version, version_comment, results) def result(method, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None ): return core_features().result(method, version, result, result_comment, reason, reason_comment, properties) def header( authserv_id = None, authserv_id_comment = None, version = None, version_comment = None, results = None ): return core_features().header( authserv_id, authserv_id_comment, version, version_comment, results) def parse(string): return core_features().parse(string) def parse_value(string): return core_features().parse_value(string) # vim:sw=4 sts=4 authres-1.2.0/authres/__main__.py0000644000175000017500000000216413521356646020432 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2011-2013 Julian Mehnle , # Copyright © 2011-2013 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. """ Package for parsing ``Authentication-Results`` headers as defined in RFC 5451/7001/7601. Optional support for authentication methods defined in RFCs 5617, 6008, 6212, 7281, 7489, and draft-ietf-dmarc-arc-protocol-05. """ __author__ = 'Julian Mehnle, Scott Kitterman' __email__ = 'scott@kitterman.com' def _test(): import doctest import authres doctest.testfile("tests") return doctest.testmod(authres) _test() # vim:sw=4 sts=4 authres-1.2.0/authres/arc.py0000644000175000017500000000776713521357320017463 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2017 Gene Shuman , # Copyright © 2012-2013, 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. """ authres extension module for the Authenticated Received Chain (ARC) RFC 8617 authentication method. """ #MODULE = 'authres' __author__ = 'Scott Kitterman, Gene Shuman' __email__ = 'scott@kitterman.com, gene@valimail.com' import authres.core from authres.core import make_result_class_properties class ARCAuthenticationResult(authres.core.AuthenticationResult): """ ARC RFC 8617 result clause of an ``Authentication-Results`` header. """ METHOD = 'arc' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, header_ams_d = None, header_ams_d_comment = None, header_ams_s = None, header_ams_s_comment = None, header_as_d = None, header_as_d_comment = None, header_as_s = None, header_as_s_comment = None, header_oldest_pass = None, header_oldest_pass_comment = None, smtp_remote_ip = None, smtp_remote_ip_comment = None, ): authres.core.AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if header_ams_d: self.header_ams_d = header_ams_d if header_ams_d_comment: self.header_ams_d_comment = header_ams_d_comment if header_ams_s: self.header_ams_s = header_ams_s if header_ams_s_comment: self.header_ams_s_comment = header_ams_s_comment if header_as_d: self.header_as_d = header_as_d if header_as_d_comment: self.header_as_d_comment = header_as_d_comment if header_as_s: self.header_as_s = header_as_s if header_as_s_comment: self.header_as_s_comment = header_as_s_comment if header_oldest_pass: self.header_oldest_pass = header_oldest_pass if header_oldest_pass_comment: self.header_oldest_pass_comment = header_oldest_pass_comment if smtp_remote_ip: self.smtp_remote_ip = smtp_remote_ip if smtp_remote_ip_comment: self.smtp_remote_ip_comment = smtp_remote_ip_comment header_ams_d, header_ams_d_comment = make_result_class_properties('header', 'ams-d') header_ams_s, header_ams_s_comment = make_result_class_properties('header', 'ams-s') header_as_d, header_ams_d_comment = make_result_class_properties('header', 'as-d') header_as_s, header_ams_s_comment = make_result_class_properties('header', 'as-s') header_oldest_pass, header_oldest_pass_comment = make_result_class_properties('header', 'oldest-pass') smtp_remote_ip, smtp_remote_ip_comment = make_result_class_properties('smtp', 'remote-ip') RESULT_CLASSES = [ ARCAuthenticationResult ] # vim:sw=4 sts=4 authres-1.2.0/authres/core.py0000644000175000017500000010267013521357147017642 0ustar kittermakitterma00000000000000# 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 authres-1.2.0/authres/dkim_adsp.py0000644000175000017500000000362413521356646020647 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2012-2013 Julian Mehnle , # Copyright © 2012-2013 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. """ authres extension module for the RFC 5617 DKIM/ADSP authentication method. """ #MODULE = 'authres' __author__ = 'Scott Kitterman, Julian Mehnle' __email__ = 'scott@kitterman.com' import authres.core from authres.core import make_result_class_properties class DKIMADSPAuthenticationResult(authres.core.AuthenticationResult): "DKIM ADSP (RFC 5617) result clause of an ``Authentication-Results`` header" METHOD = 'dkim-adsp' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, header_from = None, header_from_comment = None ): authres.core.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 header_from, header_from_comment = make_result_class_properties('header', 'from') RESULT_CLASSES = [ DKIMADSPAuthenticationResult ] # vim:sw=4 sts=4 authres-1.2.0/authres/dkim_b.py0000644000175000017500000000547413521356646020146 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2012-2013 Julian Mehnle , # Copyright © 2012-2013 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. """ authres extension module for RFC 6008 DKIM signature identification (header.b). """ #MODULE = 'authres' __author__ = 'Scott Kitterman, Julian Mehnle' __email__ = 'scott@kitterman.com' import authres.core from authres.core import make_result_class_properties class DKIMAuthenticationResult(authres.core.DKIMAuthenticationResult): "DKIM result clause of an ``Authentication-Results`` header" 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_b = None, header_b_comment = None ): authres.core.DKIMAuthenticationResult.__init__(self, version, result, result_comment, reason, reason_comment, properties, header_d, header_d_comment, header_i, header_i_comment) if header_b: self.header_b = header_b if header_b_comment: self.header_b_comment = header_b_comment header_b, header_b_comment = make_result_class_properties('header', 'b') def match_signature(self, signature_d, signature_b = None, strict = False): """Match authentication result against a DKIM signature by ``header.d`` and, if available, ``header.b``, per RFC 6008, section 4 . If ``header.b`` is absent from the authentication result, a non-strict match succeeds, whereas a strict match fails.""" if self.header_d != signature_d: return False if self.header_b is None: return not strict if len(self.header_b) >= 8: # Require prefix match: return signature_b.startswith(self.header_b) else: # Require exact match: return self.header_b == signature_b RESULT_CLASSES = [ DKIMAuthenticationResult ] # vim:sw=4 sts=4 authres-1.2.0/authres/dmarc.py0000644000175000017500000000436513521356646020005 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2012-2013 Julian Mehnle , # Copyright © 2012-2013, 2019 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. """ authres extension module for the DMARC RFC 7489 authentication method. """ #MODULE = 'authres' __author__ = 'Scott Kitterman, Julian Mehnle' __email__ = 'scott@kitterman.com' import authres.core from authres.core import make_result_class_properties class DMARCAuthenticationResult(authres.core.AuthenticationResult): """ DMARC RFC 7489 result clause of an ``Authentication-Results`` header""" METHOD = 'dmarc' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, header_from = None, header_from_comment = None, policy = None, policy_comment = None ): authres.core.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 policy: self.policy = policy if policy_comment: self.policy_comment = policy_comment header_from, header_from_comment = make_result_class_properties('header', 'from') policy, policy_comment = make_result_class_properties('policy', 'dmarc') RESULT_CLASSES = [ DMARCAuthenticationResult ] # vim:sw=4 sts=4 authres-1.2.0/authres/rrvs.py0000644000175000017500000000367213521356646017713 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2012-2013 Julian Mehnle , # Copyright © 2012-2015 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. """ authres extension module for RFC 7293, The Require-Recipient-Valid-Since Header Field and SMTP Service Extension, header field type. """ #MODULE = 'authres' __author__ = 'Scott Kitterman, Julian Mehnle' __email__ = 'scott@kitterman.com' import authres.core from authres.core import make_result_class_properties class RRVSAuthenticationResult(authres.core.AuthenticationResult): "RRVS (RFC 7293) result clause of an ``Authentication-Results`` header" METHOD = 'rrvs' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, smtp_rrvs = None, smtp_rrvs_comment = None ): authres.core.AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if smtp_rrvs: self.smtp_rrvs = smtp_rrvs if smtp_rrvs_comment: self.smtp_rrvs_comment = smtp_rrvs_comment smtp_rrvs, smtp_rrvs_comment = make_result_class_properties('smtp', 'rrvs') RESULT_CLASSES = [ RRVSAuthenticationResult ] # vim:sw=4 sts=4 authres-1.2.0/authres/smime.py0000644000175000017500000000721113521356646020022 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2012-2013 Julian Mehnle , # Copyright © 2012-2014 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. """ authres extension module for the RFC 7281, Authentication-Results Registration for S/MIME Signature Verification authentication method. """ #MODULE = 'authres' __author__ = 'Scott Kitterman, Julian Mehnle' __email__ = 'scott@kitterman.com' import authres.core from authres.core import make_result_class_properties class SMIMEAuthenticationResult(authres.core.AuthenticationResult): """ S/MIME (RFC 7281, Authentication-Results Registration for S/MIME Signature Verification) result clause of an ``Authentication-Results`` header""" METHOD = 'smime' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, body_smime_identifier = None, body_smime_identifier_comment = None, body_smime_part = None, body_smime_part_comment = None, body_smime_serial = None, body_smime_serial_comment = None, body_smime_issuer = None, body_smime_issuer_comment = None ): authres.core.AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if body_smime_identifier: self.body_smime_identifier = body_smime_identifier if body_smime_identifier_comment: self.body_smime_identifier_comment = body_smime_identifier_comment if body_smime_part: self.body_smime_part = body_smime_part if body_smime_part_comment: self.body_smime_part_comment = body_smime_part_comment if body_smime_serial: self.body_smime_serial = body_smime_serial if body_smime_serial_comment: self.body_smime_serial_comment = body_smime_serial_comment if body_smime_issuer: self.body_smime_issuer = body_smime_issuer if body_smime_issuer_comment: self.body_smime_issuer_comment = body_smime_issuer_comment # RFC 7281 Section 3.2.3 if body_smime_serial and not body_smime_issuer: raise AuthResError('body.smime-serial present, but body.smime-issuer missing.') if not body_smime_serial and body_smime_issuer: raise AuthResError('body.smime-issuer present, but body.smime-serial missing.') body_smime_identifier, body_smime_identifier_comment = make_result_class_properties('body', 'smime-identifier') body_smime_part, body_smime_part_comment = make_result_class_properties('body', 'smime-part') body_smime_serial, body_smime_serial_comment = make_result_class_properties('body', 'smime-serial') body_smime_issuer, body_smime_issuer_comment = make_result_class_properties('body', 'smime-issuer') RESULT_CLASSES = [ SMIMEAuthenticationResult ] # vim:sw=4 sts=4 authres-1.2.0/authres/tests0000644000175000017500000007345513521356646017440 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2011-2012 Julian Mehnle , # Copyright © 2011-2013 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. """ Package for parsing ``Authentication-Results`` headers as defined in RFC 5451. Optional support for authentication methods defined in RFCs 5617, 6008, 6212, 7489 and draft-ietf-dmarc-arc-protocol-05. >>> import authres >>> str(authres.AuthenticationResultsHeader('test.example.org', version=1)) 'Authentication-Results: test.example.org 1; none' Non-RFC example of no authentication with comment: >>> import authres >>> str(authres.AuthenticationResultsHeader(authserv_id = 'test.example.org', ... results = [authres.NoneAuthenticationResult(comment = 'SPF not checked for localhost')])) 'Authentication-Results: test.example.org; none (SPF not checked for localhost)' >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; spf=pass smtp.mailfrom=example.net') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'spf=pass smtp.mailfrom=example.net' >>> str(arobj.results[0].method) 'spf' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].smtp_mailfrom) 'example.net' >>> str(arobj.results[0].smtp_helo) 'None' >>> str(arobj.results[0].reason) 'None' >>> str(arobj.results[0].properties[0].type) 'smtp' >>> str(arobj.results[0].properties[0].name) 'mailfrom' >>> str(arobj.results[0].properties[0].value) 'example.net' # Missing parsing header comment. #FIXME >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; auth=pass (cram-md5) smtp.auth=sender@example.net; spf=pass smtp.mailfrom=example.net') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'auth=pass smtp.auth=sender@example.net' >>> str(arobj.results[0].method) 'auth' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].smtp_auth) 'sender@example.net' >>> str(arobj.results[0].properties[0].type) 'smtp' >>> str(arobj.results[0].properties[0].name) 'auth' >>> str(arobj.results[0].properties[0].value) 'sender@example.net' >>> str(arobj.results[1]) 'spf=pass smtp.mailfrom=example.net' >>> str(arobj.results[1].method) 'spf' >>> str(arobj.results[1].result) 'pass' >>> str(arobj.results[1].smtp_mailfrom) 'example.net' >>> str(arobj.results[1].properties[0].type) 'smtp' >>> str(arobj.results[1].properties[0].name) 'mailfrom' >>> str(arobj.results[1].properties[0].value) 'example.net' >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; sender-id=pass header.from=example.com') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'sender-id=pass header.from=example.com' >>> str(arobj.results[0].method) 'sender-id' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].header_from) 'example.com' >>> try: ... str(arobj.results[0].smtp_mailfrom) ... except AttributeError as x: ... print(x) 'SenderIDAuthenticationResult' object has no attribute 'smtp_mailfrom' >>> str(arobj.results[0].properties[0].type) 'header' >>> str(arobj.results[0].properties[0].name) 'from' >>> str(arobj.results[0].properties[0].value) 'example.com' # Missing parsing header comment. #FIXME >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; sender-id=fail header.from=example.com; dkim=pass (good signature) header.i=sender@example.com') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'sender-id=fail header.from=example.com' >>> str(arobj.results[0].method) 'sender-id' >>> str(arobj.results[0].result) 'fail' >>> str(arobj.results[0].header_from) 'example.com' >>> str(arobj.results[0].properties[0].type) 'header' >>> str(arobj.results[0].properties[0].name) 'from' >>> str(arobj.results[0].properties[0].value) 'example.com' >>> str(arobj.results[1]) 'dkim=pass header.i=sender@example.com' >>> str(arobj.results[1].method) 'dkim' >>> str(arobj.results[1].result) 'pass' >>> str(arobj.results[1].header_i) 'sender@example.com' >>> str(arobj.results[1].properties[0].type) 'header' >>> str(arobj.results[1].properties[0].name) 'i' >>> str(arobj.results[1].properties[0].value) 'sender@example.com' RFC 5451 B.6(1) modified to use d= instead of i= >>> import authres >>> dar_pass = authres.DKIMAuthenticationResult(result = 'pass', result_comment = 'good signature', ... header_d = 'mail-router.example.net') >>> dar_fail = authres.DKIMAuthenticationResult(result = 'fail', ... header_d = 'newyork.example.com', result_comment = 'bad signature') >>> str(authres.AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [dar_pass, dar_fail])) 'Authentication-Results: example.com; dkim=pass (good signature) header.d=mail-router.example.net; dkim=fail (bad signature) header.d=newyork.example.com' >>> dar_pass.match_signature('mail-router.example.net') True >>> dar_fail.match_signature('mail-router.example.net') False RFC 5451 B.6(1) modified to use d= instead of i= with header.a and header.s added >>> import authres >>> dsr_pass = authres.DKIMAuthenticationResult(result = 'pass', result_comment = 'good signature', ... header_d = 'mail-router.example.net', header_a = 'rsa-sha256', header_s = 'default') >>> dsr_fail = authres.DKIMAuthenticationResult(result = 'fail', ... header_d = 'newyork.example.com', result_comment = 'bad signature') >>> str(authres.AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [dsr_pass, dsr_fail])) 'Authentication-Results: example.com; dkim=pass (good signature) header.d=mail-router.example.net header.a=rsa-sha256 header.s=default; dkim=fail (bad signature) header.d=newyork.example.com' >>> dsr_pass.match_signature_algorithm('mail-router.example.net', 'rsa-sha256') True >>> dsr_fail.match_signature_algorithm('mail-router.example.net', 'rsa-sha256') False Header from dcrup testing >>> import authres >>> dss_pass = authres.DKIMAuthenticationResult(result = 'pass', result_comment = 'Good 256 bit ed25519-sha256 signature.', ... header_d = 'example.com', header_i = '@example.com', header_a = 'ed25519-sha256') >>> dss_fail = authres.DKIMAuthenticationResult(result = 'fail', result_comment = 'Bad 1024 bit rsa-sha256 signature.', ... header_d = 'example.com', header_i = '@example.com', header_a = 'rsa-sha256') >>> str(authres.AuthenticationResultsHeader(authserv_id = 'relay02.example.org', ... results = [dss_pass, dss_fail])) 'Authentication-Results: relay02.example.org; dkim=pass (Good 256 bit ed25519-sha256 signature.) header.d=example.com header.i=@example.com header.a=ed25519-sha256; dkim=fail (Bad 1024 bit rsa-sha256 signature.) header.d=example.com header.i=@example.com header.a=rsa-sha256' # Missing parsing header comment. #FIXME >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; dkim=pass (good signature) header.i=@mail-router.example.net; dkim=fail (bad signature) header.i=@newyork.example.com') >>> str(arobj.results[0]) 'dkim=pass header.i=@mail-router.example.net' >>> str(arobj.results[0].method) 'dkim' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].header_i) '@mail-router.example.net' >>> str(arobj.results[0].properties[0].type) 'header' >>> str(arobj.results[0].properties[0].name) 'i' >>> str(arobj.results[0].properties[0].value) '@mail-router.example.net' >>> str(arobj.results[1]) 'dkim=fail header.i=@newyork.example.com' >>> str(arobj.results[1].method) 'dkim' >>> str(arobj.results[1].result) 'fail' >>> str(arobj.results[1].header_i) '@newyork.example.com' >>> str(arobj.results[1].properties[0].type) 'header' >>> str(arobj.results[1].properties[0].name) 'i' >>> str(arobj.results[1].properties[0].value) '@newyork.example.com' RFC 5451bis C.6(1) modified to use d= instead of i= >>> import authres >>> dar_pass = authres.DKIMAuthenticationResult(result = 'pass', reason = 'good signature', ... header_d = 'mail-router.example.net') >>> dar_fail = authres.DKIMAuthenticationResult(result = 'fail', ... header_d = 'newyork.example.com', reason = 'bad signature') >>> str(authres.AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [dar_pass, dar_fail])) 'Authentication-Results: example.com; dkim=pass reason="good signature" header.d=mail-router.example.net; dkim=fail reason="bad signature" header.d=newyork.example.com' >>> dar_pass.match_signature('mail-router.example.net') True >>> dar_fail.match_signature('mail-router.example.net') False # Missing parsing header comment. #FIXME >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; dkim=pass reason="good signature" header.i=@mail-router.example.net; dkim=fail reason="bad signature" header.i=@newyork.example.com') >>> str(arobj.results[0]) 'dkim=pass reason="good signature" header.i=@mail-router.example.net' >>> str(arobj.results[0].method) 'dkim' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].reason) 'good signature' >>> str(arobj.results[0].header_i) '@mail-router.example.net' >>> str(arobj.results[0].properties[0].type) 'header' >>> str(arobj.results[0].properties[0].name) 'i' >>> str(arobj.results[0].properties[0].value) '@mail-router.example.net' >>> str(arobj.results[1]) 'dkim=fail reason="bad signature" header.i=@newyork.example.com' >>> str(arobj.results[1].method) 'dkim' >>> str(arobj.results[1].result) 'fail' >>> str(arobj.results[1].reason) 'bad signature' >>> str(arobj.results[1].header_i) '@newyork.example.com' >>> str(arobj.results[1].properties[0].type) 'header' >>> str(arobj.results[1].properties[0].name) 'i' >>> str(arobj.results[1].properties[0].value) '@newyork.example.com' RFC 5451 B.6(2) >>> import authres >>> str(authres.AuthenticationResultsHeader(authserv_id = 'example.net', ... results = [authres.DKIMAuthenticationResult(result = 'pass', result_comment = 'good signature', ... header_i = '@newyork.example.com')])) 'Authentication-Results: example.net; dkim=pass (good signature) header.i=@newyork.example.com' # Missing parsing header comment. #FIXME import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.net; dkim=pass (good signature) header.i=@newyork.example.com') >>> str(arobj.results[0]) 'dkim=pass header.i=@newyork.example.com' >>> str(arobj.results[0].method) 'dkim' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].header_i) '@newyork.example.com' >>> str(arobj.results[0].properties[0].type) 'header' >>> str(arobj.results[0].properties[0].name) 'i' >>> str(arobj.results[0].properties[0].value) '@newyork.example.com' RFC 6008 A.1 >>> import authres >>> import authres.dkim_b >>> authres_context = authres.FeatureContext(authres.dkim_b) >>> dar_b = authres.dkim_b.DKIMAuthenticationResult(result = 'pass', result_comment = 'good signature', ... header_d = 'newyork.example.com', header_b = 'oINEO8hg') >>> str(authres_context.header(authserv_id = 'mail-router.example.net', ... results = [dar_b, authres.dkim_b.DKIMAuthenticationResult(result = 'fail', ... header_d = 'newyork.example.com', result_comment = 'bad signature', header_b = 'EToRSuvU')])) 'Authentication-Results: mail-router.example.net; dkim=pass (good signature) header.d=newyork.example.com header.b=oINEO8hg; dkim=fail (bad signature) header.d=newyork.example.com header.b=EToRSuvU' RFC 6008 section 4 >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b,'newyork.example.com','oINEO8h') False >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b,'newyork.example.com','oINEO8hg') True >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b,'newyork.example.com','oINEO8hq') False >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b,'newyork.example.com','oINEO8hq1') False >>> dar_b2 = authres.dkim_b.DKIMAuthenticationResult(result = 'pass', result_comment = 'good signature', ... header_d = 'newyork.example.com', header_b = 'oINEO8') >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b2,'newyork.example.com','oINEO8') True >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b2,'newyork.example.com','oINEO8hq') False >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b2,'newyork.example.com', None) False >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b2,'newyork.example.com', None) False >>> dar_b_none = authres.dkim_b.DKIMAuthenticationResult(result = 'pass', result_comment = 'good signature', ... header_d = 'newyork.example.com') >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b_none,'newyork.example.com', None) True >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b_none,'newyork.example.com', None, strict=True) False >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b_none,'newyork.example.com','oINEO8hq') True >>> authres.dkim_b.DKIMAuthenticationResult.match_signature(dar_b_none,'newjersey.example.com', None) False # Missing parsing header comment. #FIXME >>> arobj = authres_context.parse('Authentication-Results: mail-router.example.net; dkim=pass (good signature) header.d=newyork.example.com header.b=oINEO8hg; dkim=fail (bad signature) header.d=newyork.example.com header.b=EToRSuvU') >>> str(arobj.results[0]) 'dkim=pass header.d=newyork.example.com header.b=oINEO8hg' >>> str(arobj.results[0].method) 'dkim' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].header_d) 'newyork.example.com' >>> str(arobj.results[0].properties[0].type) 'header' >>> str(arobj.results[0].properties[0].name) 'd' >>> str(arobj.results[0].properties[0].value) 'newyork.example.com' >>> str(arobj.results[0].header_b) 'oINEO8hg' >>> str(arobj.results[0].properties[1].type) 'header' >>> str(arobj.results[0].properties[1].name) 'b' >>> str(arobj.results[0].properties[1].value) 'oINEO8hg' >>> str(arobj.results[1].method) 'dkim' >>> str(arobj.results[1].result) 'fail' >>> str(arobj.results[1].header_d) 'newyork.example.com' >>> str(arobj.results[1].properties[0].type) 'header' >>> str(arobj.results[1].properties[0].name) 'd' >>> str(arobj.results[1].properties[0].value) 'newyork.example.com' >>> str(arobj.results[1].header_b) 'EToRSuvU' >>> str(arobj.results[1].properties[1].type) 'header' >>> str(arobj.results[1].properties[1].name) 'b' >>> str(arobj.results[1].properties[1].value) 'EToRSuvU' # RFC 5617 (based on RFC text, no examples provided) >>> import authres >>> import authres.dkim_adsp >>> authres_context = authres.FeatureContext(authres.dkim_adsp) >>> str(authres_context.header(authserv_id = 'example.com', ... results = [authres.DKIMAuthenticationResult(result = 'fail', result_comment = 'bad signature', ... header_d = 'bank.example.net'), authres.dkim_adsp.DKIMADSPAuthenticationResult(result = 'discard', ... header_from = 'phish@bank.example.com', result_comment = 'From domain and d= domain match')])) 'Authentication-Results: example.com; dkim=fail (bad signature) header.d=bank.example.net; dkim-adsp=discard (From domain and d= domain match) header.from=phish@bank.example.com' # Missing parsing header comment. #FIXME >>> arobj = authres_context.parse('Authentication-Results: example.com; dkim=fail (bad signature) header.d=bank.example.net; dkim-adsp=discard (From domain and d= domain match) header.from=phish@bank.example.com') >>> str(arobj.results[1]) 'dkim-adsp=discard header.from=phish@bank.example.com' >>> str(arobj.results[1].method) 'dkim-adsp' >>> str(arobj.results[1].result) 'discard' >>> str(arobj.results[1].header_from) 'phish@bank.example.com' >>> str(arobj.results[1].properties[0].type) 'header' >>> str(arobj.results[1].properties[0].name) 'from' >>> str(arobj.results[1].properties[0].value) 'phish@bank.example.com' RFC 6212 A.1 >>> import authres >>> import authres.dkim_b, authres.vbr >>> authres_context = authres.FeatureContext(authres.dkim_b, authres.vbr) >>> str(authres_context.header(authserv_id = 'mail-router.example.net', ... results = [authres.dkim_b.DKIMAuthenticationResult(result = 'pass', result_comment = 'good signature', ... header_d = 'newyork.example.com', header_b = 'oINEO8hg'), authres.vbr.VBRAuthenticationResult(result = 'pass', ... header_md = 'newyork.example.com', result_comment = 'voucher.example.net', ... header_mv = 'voucher.example.org')])) 'Authentication-Results: mail-router.example.net; dkim=pass (good signature) header.d=newyork.example.com header.b=oINEO8hg; vbr=pass (voucher.example.net) header.md=newyork.example.com header.mv=voucher.example.org' # Missing parsing header comment. #FIXME >>> arobj = authres_context.parse('Authentication-Results: mail-router.example.net; dkim=pass (good signature) header.d=newyork.example.com header.b=oINEO8hg; vbr=pass (voucher.example.net) header.md=newyork.example.com header.mv=voucher.example.org') >>> str(arobj.results[1]) 'vbr=pass header.md=newyork.example.com header.mv=voucher.example.org' >>> str(arobj.results[1].method) 'vbr' >>> str(arobj.results[1].result) 'pass' >>> str(arobj.results[1].header_md) 'newyork.example.com' >>> str(arobj.results[1].properties[0].type) 'header' >>> str(arobj.results[1].properties[0].name) 'md' >>> str(arobj.results[1].properties[0].value) 'newyork.example.com' >>> str(arobj.results[1].header_mv) 'voucher.example.org' >>> str(arobj.results[1].properties[1].type) 'header' >>> str(arobj.results[1].properties[1].name) 'mv' >>> str(arobj.results[1].properties[1].value) 'voucher.example.org' # RFC 7489 DMARC example from opendmarc >>> import authres >>> import authres.dmarc >>> new_context = authres.FeatureContext(authres.dmarc) >>> str(new_context.header(authserv_id = 'mail-router.example.net', ... results = [authres.dmarc.DMARCAuthenticationResult(result = 'pass', ... header_from = 'example.com')])) 'Authentication-Results: mail-router.example.net; dmarc=pass header.from=example.com' # Missing parsing header comment. #FIXME >>> newarobj = new_context.parse('Authentication-Results: mail-router.example.net; dmarc=pass header.from=example.com') >>> str(newarobj.results[0]) 'dmarc=pass header.from=example.com' >>> str(newarobj.results[0].method) 'dmarc' >>> str(newarobj.results[0].result) 'pass' >>> str(newarobj.results[0].header_from) 'example.com' >>> str(newarobj.results[0].properties[0].type) 'header' >>> str(newarobj.results[0].properties[0].name) 'from' >>> str(newarobj.results[0].properties[0].value) 'example.com' # Non-RFC DMARC example with policy included >>> import authres >>> import authres.dmarc >>> new_context = authres.FeatureContext(authres.dmarc) >>> str(new_context.header(authserv_id='mail-router.example.net', ... results = [authres.dmarc.DMARCAuthenticationResult(result='pass', policy='none', ... header_from='example.com')])) 'Authentication-Results: mail-router.example.net; dmarc=pass header.from=example.com policy.dmarc=none' # Non-RFC - SPF with localpart in pvalue >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; spf=pass smtp.mailfrom=authenticated@example.net') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'spf=pass smtp.mailfrom=authenticated@example.net' >>> str(arobj.results[0].method) 'spf' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].smtp_mailfrom) 'authenticated@example.net' >>> str(arobj.results[0].smtp_helo) 'None' >>> str(arobj.results[0].reason) 'None' >>> str(arobj.results[0].properties[0].type) 'smtp' >>> str(arobj.results[0].properties[0].name) 'mailfrom' >>> str(arobj.results[0].properties[0].value) 'authenticated@example.net' # None RFC - Separate reporting of SPF Mail From and HELO results >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; spf=pass smtp.mailfrom=authenticated@example.net; spf=none smtp.helo=mailserver.example.net') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'spf=pass smtp.mailfrom=authenticated@example.net' >>> str(arobj.results[0].method) 'spf' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].smtp_mailfrom) 'authenticated@example.net' >>> str(arobj.results[0].smtp_helo) 'None' >>> str(arobj.results[0].reason) 'None' >>> str(arobj.results[0].properties[0].type) 'smtp' >>> str(arobj.results[0].properties[0].name) 'mailfrom' >>> str(arobj.results[0].properties[0].value) 'authenticated@example.net' >>> str(arobj.results[1]) 'spf=none smtp.helo=mailserver.example.net' >>> str(arobj.results[1].method) 'spf' >>> str(arobj.results[1].result) 'none' >>> str(arobj.results[1].smtp_mailfrom) 'None' >>> str(arobj.results[1].smtp_helo) 'mailserver.example.net' >>> str(arobj.results[1].reason) 'None' >>> str(arobj.results[1].properties[0].type) 'smtp' >>> str(arobj.results[1].properties[0].name) 'helo' >>> str(arobj.results[1].properties[0].value) 'mailserver.example.net' # Create header field with multiple SPF results >>> import authres >>> mfrom_pass = authres.SPFAuthenticationResult(result = 'pass', ... smtp_mailfrom = 'authenticated@example.net') >>> helo_none = authres.SPFAuthenticationResult(result = 'none', ... smtp_helo = 'mailserver.example.net', reason = 'No SPF record for HELO') >>> str(authres.AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [mfrom_pass, helo_none])) 'Authentication-Results: example.com; spf=pass smtp.mailfrom=authenticated@example.net; spf=none reason="No SPF record for HELO" smtp.helo=mailserver.example.net' # Create header field with ARC results (draft-ietf-dmarc-arc-protocol-18) >>> import authres >>> import authres.arc >>> arc_pass = authres.arc.ARCAuthenticationResult(result = 'pass', ... header_ams_d = 'example.net', header_ams_s='valimail2016', header_as_d="example.com", header_as_s="valimail2017", header_oldest_pass='1', smtp_remote_ip='203.0.113.1') >>> str(authres.AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [arc_pass])) 'Authentication-Results: example.com; arc=pass header.ams-d=example.net header.ams-s=valimail2016 header.as-d=example.com header.as-s=valimail2017 header.oldest-pass=1 smtp.remote-ip=203.0.113.1' # Parsing IP6 address. >>> arobj = authres_context.parse('Authentication-Results: mail.bmsi.com; iprev=pass policy.iprev="2001:748:100:40::2:2" (mout0.freenet.de); spf=none smtp.mailfrom=markuslaudi@freenet.de') >>> str(arobj.results[0]) 'iprev=pass policy.iprev="2001:748:100:40::2:2"' >>> arobj.results[0].policy_iprev '2001:748:100:40::2:2' # Parsing iprev with unquoted IP6 address should fail. >>> try: ... arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: mail.bmsi.com; iprev=pass policy.iprev=2001:748:100:40::2:2 (mout0.freenet.de); spf=none smtp.mailfrom=markuslaudi@freenet.de') ... except authres.SyntaxError as x: ... print(x) Syntax error: Expected end of text at: :748:100:40::2:2 (mout0.freenet.de); spf... # Generating iprev with IP6 address. >>> iprev_pass = authres.IPRevAuthenticationResult(result = 'pass', policy_iprev = '2001:db8:ea1::dead:beef', policy_iprev_comment='yummy.example.net') >>> str(authres.AuthenticationResultsHeader(authserv_id='example.com',results = [iprev_pass])) 'Authentication-Results: example.com; iprev=pass policy.iprev="2001:db8:ea1::dead:beef" (yummy.example.net)' # RFC 7281, Authentication-Results Registration for S/MIME Signature Verification >>> import authres.smime >>> mimearobj = authres_context.parse('Authentication-Results: example.net; smime=fail (certificate is revoked by CRL) body.smime-identifier=aliceDss@example.com body.smime-part=2') >>> str(mimearobj.results[0]) 'smime=fail body.smime-identifier=aliceDss@example.com body.smime-part=2' >>> str(mimearobj.authserv_id) 'example.net' >>> str(mimearobj.results[0].method) 'smime' >>> str(mimearobj.results[0].result) 'fail' >>> str(mimearobj.results[0].reason) 'None' >>> str(mimearobj.results[0].properties[0].type) 'body' >>> str(mimearobj.results[0].properties[0].name) 'smime-identifier' >>> str(mimearobj.results[0].properties[0].value) 'aliceDss@example.com' >>> str(mimearobj.results[0].properties[1].type) 'body' >>> str(mimearobj.results[0].properties[1].name) 'smime-part' >>> str(mimearobj.results[0].properties[1].value) '2' >>> import authres.smime >>> smime_fail = authres.smime.SMIMEAuthenticationResult(result = 'fail', result_comment = 'certificate is revoked by CRL', body_smime_identifier = 'aliceDss@example.com', body_smime_part = '2') >>> str(authres.AuthenticationResultsHeader(authserv_id='example.net',results = [smime_fail])) 'Authentication-Results: example.net; smime=fail (certificate is revoked by CRL) body.smime-identifier=aliceDss@example.com body.smime-part=2' """ # RFC 7293, The Require-Recipient-Valid-Since Header Field and SMTP Service Extension, header field types # Example 12.3 from RFC 7293 >>> import authres.rrvs >>> rrvsarobj = authres_context.parse('Authentication-Results: mx.example.com; rrvs=pass smtp.rcptto=user@example.com') >>> str(rrvsarobj.authserv_id) 'mx.example.com' >>> str(rrvsarobj.results[0].method) 'rrvs' >>> str(rrvsarobj.results[0].result) 'pass' >>> str(rrvsarobj.results[0].reason) 'None' >>> str(rrvsarobj.results[0].properties[0].type) 'smtp' >>> str(rrvsarobj.results[0].properties[0].name) 'rcptto' >>> str(rrvsarobj.results[0].properties[0].value) 'user@example.com' >>> import authres.rrvs >>> rrvs_fail = authres.rrvs.RRVSAuthenticationResult(result = 'fail', result_comment = 'Mail box expired.', smtp_rrvs = 'user@example.com', smtp_rrvs_comment = "These are not the droids you're looking for.") >>> str(authres.AuthenticationResultsHeader(authserv_id='example.net',results = [rrvs_fail])) "Authentication-Results: example.net; rrvs=fail (Mail box expired.) smtp.rrvs=user@example.com (These are not the droids you're looking for.)" # New for RFC 7601 SMTP Auth Mail From >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: example.com; auth=pass smtp.mailfrom=sender@example.net') >>> str(arobj.authserv_id) 'example.com' >>> str(arobj.results[0]) 'auth=pass smtp.mailfrom=sender@example.net' >>> str(arobj.results[0].method) 'auth' >>> str(arobj.results[0].result) 'pass' >>> str(arobj.results[0].smtp_mailfrom) 'sender@example.net' >>> str(arobj.results[0].properties[0].type) 'smtp' >>> str(arobj.results[0].properties[0].name) 'mailfrom' >>> str(arobj.results[0].properties[0].value) 'sender@example.net' # Create header field with RFC 7601 SMTP Auth Mail From >>> import authres >>> mfrom_auth = authres.SMTPAUTHAuthenticationResult(result = 'pass', ... smtp_mailfrom = 'mailauth@example.net') >>> str(authres.AuthenticationResultsHeader(authserv_id = 'example.com', ... results = [mfrom_auth])) 'Authentication-Results: example.com; auth=pass smtp.mailfrom=mailauth@example.net' # Ignore unknown method (RFC 7601 2.7.6 SHOULD) >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: mail.example.org; tls=pass smtp.TLSversion=TLSv1.2 smtp.TLScyper=ECDHE-RSA-CHACHA20-POLY1305 smtp.TLSbits=256') >>> str(arobj.authserv_id) 'mail.example.org' >>> str(arobj.results[0]) 'tls=pass smtp.tlsversion=TLSv1.2 smtp.tlscyper=ECDHE-RSA-CHACHA20-POLY1305 smtp.tlsbits=256' >>> str(arobj.results[0].method) 'tls' >>> str(arobj.results[0].result) 'pass' >>> try: str(arobj.results[0].smtp_mailfrom) ... except AttributeError as e: print(e) 'AuthenticationResult' object has no attribute 'smtp_mailfrom' >>> str(arobj.results[0].properties[0].type) 'smtp' >>> str(arobj.results[0].properties[0].name) 'tlsversion' >>> str(arobj.results[0].properties[0].value) 'TLSv1.2' # Invalid ptype (error) and unknown method (OK) >>> import authres >>> try: arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: mail.example.org; x-tls=pass version=TLSv1.2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256') ... except authres.SyntaxError as e: print(e) >>> str(arobj.authserv_id) 'mail.example.org' >>> str(arobj.results[0]) 'x-tls=pass' # Valid ptype (OK), unknown method (OK), unknown property (OKish) >>> import authres >>> try: arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: mail.example.org; x-tls=pass smtp.tlsversion=TLSv1.2') ... except authres.SyntaxError as e: print(e) >>> str(arobj.authserv_id) 'mail.example.org' >>> str(arobj.results[0]) 'x-tls=pass smtp.tlsversion=TLSv1.2' >>> str(arobj.results[0].method) 'x-tls' >>> str(arobj.results[0].result) 'pass' >>> try: str(arobj.results[0].smtp_mailfrom) ... except AttributeError as e: print(e) 'AuthenticationResult' object has no attribute 'smtp_mailfrom' >>> try: str(arobj.results[0].smtp_tlsversion) ... except AttributeError as e: print(e) 'AuthenticationResult' object has no attribute 'smtp_tlsversion' >>> str(arobj.results[0].properties[0].type) 'smtp' >>> str(arobj.results[0].properties[0].name) 'tlsversion' >>> str(arobj.results[0].properties[0].value) 'TLSv1.2' # Parse multiple result header field and toss out unknown ptype >>> import authres >>> arobj = authres.AuthenticationResultsHeader.parse('Authentication-Results: mail.example.org; arc=none (no signatures found); dkim=pass (1024-bit rsa key sha256) header.d=example.net header.i=@example.net header.b=Qgi/FoC0; dmarc=none (p=none) header.from=example.net; spf=none smtp.mailfrom=test@example.net smtp.helo=mail-wm0-x232.example.com; x-tls=pass version=TLSv1.2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256') >>> for result in arobj.results: ... result.method ... result.result 'arc' 'none' 'dkim' 'pass' 'dmarc' 'none' 'spf' 'none' 'x-tls' 'pass' # Repeat authserv_id for each result (invalid) >>> import authres >>> try: arobj = authres.AuthenticationResultsHeader.parse('authentication-results: thehesiod.com; dkim=none (message not signed) header.d=none;thehesiod.com; dmarc=none action=none header.from=hotmail.com;') ... except authres.SyntaxError as e: print(e) Syntax error: Expected "=" at: ; dmarc=none action=none header.from=hot... # vim:sw=4 sts=4 authres-1.2.0/authres/vbr.py0000644000175000017500000000435613521356646017510 0ustar kittermakitterma00000000000000# coding: utf-8 # Copyright © 2012-2013 Julian Mehnle , # Copyright © 2012-2013 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. """ authres extension module for the RFC 6212 Vouch By Reference (VBR) authentication method. """ #MODULE = 'authres' __author__ = 'Scott Kitterman, Julian Mehnle' __email__ = 'scott@kitterman.com' import authres.core from authres.core import make_result_class_properties class VBRAuthenticationResult(authres.core.AuthenticationResult): "VBR (RFC 6212) result clause of an ``Authentication-Results`` header" METHOD = 'vbr' def __init__(self, version = None, result = None, result_comment = None, reason = None, reason_comment = None, properties = None, header_md = None, header_md_comment = None, header_mv = None, header_mv_comment = None ): authres.core.AuthenticationResult.__init__(self, self.METHOD, version, result, result_comment, reason, reason_comment, properties) if header_md: self.header_md = header_md if header_md_comment: self.header_md_comment = header_md_comment if header_mv: self.header_mv = header_mv if header_mv_comment: self.header_mv_comment = header_mv_comment header_md, header_md_comment = make_result_class_properties('header', 'md') header_mv, header_mv_comment = make_result_class_properties('header', 'mv') RESULT_CLASSES = [ VBRAuthenticationResult ] # vim:sw=4 sts=4 authres-1.2.0/authres.egg-info/0000755000175000017500000000000013521360303020010 5ustar kittermakitterma00000000000000authres-1.2.0/authres.egg-info/PKG-INFO0000644000175000017500000000171213521360303021106 0ustar kittermakitterma00000000000000Metadata-Version: 1.1 Name: authres Version: 1.2.0 Summary: authres - Authentication Results Header Module Home-page: https://launchpad.net/authentication-results-python Author: Julian Mehnle, Scott Kitterman Author-email: julian@mehnle.net License: Apache 2.0 Description: UNKNOWN Keywords: dkim,spf,dmarc,email,authentication,rfc5451,rfc7001,rfc7601,rfc8601 Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Communications :: Email :: Mail Transport Agents Classifier: Topic :: Communications :: Email :: Filters Classifier: Topic :: Software Development :: Libraries :: Python Modules authres-1.2.0/authres.egg-info/SOURCES.txt0000644000175000017500000000060213521360303021672 0ustar kittermakitterma00000000000000CHANGES COPYING MANIFEST.in README setup.py authres/__init__.py authres/__main__.py authres/arc.py authres/core.py authres/dkim_adsp.py authres/dkim_b.py authres/dmarc.py authres/rrvs.py authres/smime.py authres/tests authres/vbr.py authres.egg-info/PKG-INFO authres.egg-info/SOURCES.txt authres.egg-info/dependency_links.txt authres.egg-info/not-zip-safe authres.egg-info/top_level.txtauthres-1.2.0/authres.egg-info/dependency_links.txt0000644000175000017500000000000113521360303024056 0ustar kittermakitterma00000000000000 authres-1.2.0/authres.egg-info/not-zip-safe0000644000175000017500000000000113521353464022250 0ustar kittermakitterma00000000000000 authres-1.2.0/authres.egg-info/top_level.txt0000644000175000017500000000001013521360303022531 0ustar kittermakitterma00000000000000authres authres-1.2.0/setup.cfg0000644000175000017500000000004613521360303016464 0ustar kittermakitterma00000000000000[egg_info] tag_build = tag_date = 0 authres-1.2.0/setup.py0000755000175000017500000000340213521356646016376 0ustar kittermakitterma00000000000000# -*- coding: ISO-8859-1 # Copyright © 2011 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. from setuptools import setup setup(name='authres', version='1.2.0', description='authres - Authentication Results Header Module', author='Julian Mehnle, Scott Kitterman', author_email='julian@mehnle.net', url='https://launchpad.net/authentication-results-python', license='Apache 2.0', packages = ['authres',], include_package_data=True, package_data = {'authres': ['tests']}, keywords = ['dkim', 'spf', 'dmarc', 'email', 'authentication', 'rfc5451', 'rfc7001', 'rfc7601', 'rfc8601'], classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Communications :: Email :: Mail Transport Agents', 'Topic :: Communications :: Email :: Filters', 'Topic :: Software Development :: Libraries :: Python Modules', ], zip_safe = False, )