Source code for gs.dmarc.lookup

# -*- coding: utf-8 -*-
############################################################################
#
# Copyright © 2014, 2015, 2016 OnlineGroups.net and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
############################################################################
from __future__ import absolute_import, unicode_literals, print_function
from enum import Enum
from pkg_resources import resource_filename  # Part of setuptools
try:
    # typing is needed by mypy, but is unused otherwise
    from typing import Dict, Text  # noqa: F401
except ImportError:
    pass
from dns.resolver import (query as dns_query, NXDOMAIN, NoAnswer,
                          NoNameservers)
from publicsuffix import PublicSuffixList


[docs]class ReceiverPolicy(Enum): '''An enumeration of the different receiver policies in DMARC.''' __order__ = 'noDmarc none quarantine reject' # only needed in 2.x #: No published DMARC receiver-policy could be found. Often interpreted #: the same way as :attr:`gs.dmarc.ReceiverPolicy.none`. noDmarc = 0 #: The published policy is ``none``. Recieving parties are supposed to #: skip the verification of the DMARC signature. none = 1 #: Generally causes the message to be marked as *spam* if verification #: fails. quarantine = 2 #: Causes the system that is receiving the message to reject the #: message if the verification fails. reject = 3
def answer_to_dict(answer): # type: (Text) -> Dict[unicode, unicode] '''Turn the DNS DMARC answer into a dict of tag:value pairs.''' a = answer.strip('"').strip(' ') rawTags = [t.split('=') for t in a.split(';') if t] retval = {t[0].strip(): t[1].strip() for t in rawTags} return retval
[docs]def lookup_receiver_policy(host, policyTag='p'): # type: (str, str) -> ReceiverPolicy '''Lookup the reciever policy for a host. Returns a ReceiverPolicy. :param str host: The host to query. The *actual* host that is queried has ``_dmarc.`` prepended to it. :param str policyTag: The *tag* that holds the receiver policy. Must be ``p`` (the default) or ``sp`` (for the subdomain policy). See :rfc:`7489#section-6.3`. :returns: The DMARC receiver policy for the host. If there is no published policy then :attr:`gs.dmarc.ReceiverPolicy.noDmarc` is returned. :rtype: A member of the :class:`gs.dmarc.ReceiverPolicy` enumeration.''' if policyTag not in ('p', 'sp'): raise ValueError('policyTag must be "p" or "sp".') dmarcHost = '_dmarc.{0}'.format(host) retval = ReceiverPolicy.noDmarc try: dnsAnswer = dns_query(dmarcHost, 'TXT') except (NXDOMAIN, NoAnswer, NoNameservers): pass # retval = ReceiverPolicy.noDmarc else: answer = str(dnsAnswer[0]) # Check that v= field is the first one in the answer (which is in # double quotes) as per Section 7.1 (5): # In particular, the "v=DMARC1" tag is mandatory and MUST appear # first in the list. Discard any that do not pass this test. # http://tools.ietf.org/html/draft-kucherawy-dmarc-base-04#section-7.1 if answer[:9] == '"v=DMARC1': tags = answer_to_dict(answer) # TODO: check that 'none' is the right assumption? if policyTag == 'p': p = tags.get('p', 'none') else: # Look up the subdomain policy, falling back to the main policy # <https://tools.ietf.org/html/rfc7489#section-6.3> p = tags.get('sp', tags.get('p', 'none')) policy = p if hasattr(ReceiverPolicy, p) else 'noDmarc' # https://github.com/python/mypy/issues/1381 retval = ReceiverPolicy[policy] # type: ignore return retval
[docs]def receiver_policy(host): # type: (str) -> ReceiverPolicy '''Get the DMARC receiver policy for a host. :param str host: The host to lookup. :returns: The DMARC reciever policy for the host. :rtype: A member of the :class:`gs.dmarc.ReceiverPolicy` enumeration. The :func:`receiver_policy` function looks up the DMARC reciever polciy for ``host``. If the host does not have a pubished policy `the organizational domain`_ is determined. The DMARC policy for the organizational domain is queried, and the subdomain policy is reuturned (if specified) or the overall policy for the domain is returned. Internally the :func:`gs.dmarc.lookup.lookup_receiver_policy` is used to perform the query. .. _the organizational domain: http://tools.ietf.org/html/draft-kucherawy-dmarc-base-04#section-3.2''' hostSansDmarc = host if host[:7] != '_dmarc.' else host[7:] retval = lookup_receiver_policy(hostSansDmarc) if retval == ReceiverPolicy.noDmarc: fn = get_suffix_list_file_name() with open(fn) as suffixList: psl = PublicSuffixList(suffixList) newHost = psl.get_public_suffix(hostSansDmarc) retval = lookup_receiver_policy(newHost, policyTag='sp') return retval
def get_suffix_list_file_name(): # type: () -> Text '''Get the file name for the public-suffix list data file :returns: The filename for the datafile in this module. :rtype: ``str``''' # TODO: automatically update the suffix list data file # <https://publicsuffix.org/list/effective_tld_names.dat> retval = resource_filename('gs.dmarc', 'suffixlist.txt') return retval