Skip to content

Commit fa93631

Browse files
committed
Update to pass the flake8 hook
Make the necessary changes to pass the `flake8` pre-commit hook. This is almost exclusively to satisfy the flake8-docstring plugin.
1 parent a8fb546 commit fa93631

File tree

4 files changed

+60
-22
lines changed

4 files changed

+60
-22
lines changed

src/trustymail/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""trustymail: A tool for scanning DNS mail records for evaluating security.
2+
23
Usage:
34
trustymail (INPUT ...) [options]
45
trustymail (INPUT ...) [--output=OUTFILE] [--timeout=TIMEOUT] [--smtp-timeout=TIMEOUT] [--smtp-localhost=HOSTNAME] [--smtp-ports=PORTS] [--no-smtp-cache] [--mx] [--starttls] [--spf] [--dmarc] [--debug] [--json] [--dns=HOSTNAMES] [--psl-filename=FILENAME] [--psl-read-only]
@@ -65,6 +66,7 @@
6566

6667

6768
def main():
69+
"""Perform a trustymail scan using the provided options."""
6870
args = docopt.docopt(__doc__, version=__version__)
6971

7072
# Monkey patching trustymail to make it cache the PSL where we want
@@ -163,6 +165,7 @@ def main():
163165

164166

165167
def write(content, out_file):
168+
"""Write the provided content to a file after ensuring all intermediate directories exist."""
166169
parent = os.path.dirname(out_file)
167170
if parent != "":
168171
mkdir_p(parent)
@@ -175,6 +178,7 @@ def write(content, out_file):
175178
# mkdir -p in python, from:
176179
# http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python
177180
def mkdir_p(path):
181+
"""Make a directory and all intermediate directories in its path."""
178182
try:
179183
os.makedirs(path)
180184
except OSError as exc: # Python >2.5

src/trustymail/domain.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Provide a data model for domains and some utility functions."""
2+
13
# Standard Python Libraries
24
from collections import OrderedDict
35
from datetime import datetime, timedelta
@@ -12,7 +14,7 @@
1214

1315
def get_psl():
1416
"""
15-
Gets the Public Suffix List - either new, or cached in the CWD for 24 hours
17+
Get the Public Suffix List - either new, or cached in the CWD for 24 hours.
1618
1719
Returns
1820
-------
@@ -42,14 +44,14 @@ def download_psl():
4244

4345

4446
def get_public_suffix(domain):
45-
"""Returns the public suffix of a given domain"""
47+
"""Return the public suffix of a given domain."""
4648
public_list = get_psl()
4749

4850
return public_list.get_public_suffix(domain)
4951

5052

5153
def format_list(record_list):
52-
"""Format a list into a string to increase readability in CSV"""
54+
"""Format a list into a string to increase readability in CSV."""
5355
# record_list should only be a list, not an integer, None, or
5456
# anything else. Thus this if clause handles only empty
5557
# lists. This makes a "null" appear in the JSON output for
@@ -61,7 +63,9 @@ def format_list(record_list):
6163

6264

6365
class Domain:
64-
base_domains: Dict[str, Domain] = {}
66+
"""Store information about a domain."""
67+
68+
base_domains: Dict[str, "Domain"] = {}
6569

6670
def __init__(
6771
self,
@@ -73,6 +77,7 @@ def __init__(
7377
smtp_cache,
7478
dns_hostnames,
7579
):
80+
"""Retrieve information about a given domain name."""
7681
self.domain_name = domain_name.lower()
7782

7883
self.base_domain_name = get_public_suffix(self.domain_name)
@@ -137,15 +142,13 @@ def __init__(
137142
self.ports_tested = set()
138143

139144
def has_mail(self):
145+
"""Check if there are any mail servers associated with this domain."""
140146
if self.mail_servers is not None:
141147
return len(self.mail_servers) > 0
142148
return None
143149

144150
def has_supports_smtp(self):
145-
"""
146-
Returns True if any of the mail servers associated with this
147-
domain are listening and support SMTP.
148-
"""
151+
"""Check if any of the mail servers associated with this domain are listening and support SMTP."""
149152
result = None
150153
if len(self.starttls_results) > 0:
151154
result = (
@@ -160,10 +163,7 @@ def has_supports_smtp(self):
160163
return result
161164

162165
def has_starttls(self):
163-
"""
164-
Returns True if any of the mail servers associated with this
165-
domain are listening and support STARTTLS.
166-
"""
166+
"""Check if any of the mail servers associated with this domain are listening and support STARTTLS."""
167167
result = None
168168
if len(self.starttls_results) > 0:
169169
result = (
@@ -178,16 +178,19 @@ def has_starttls(self):
178178
return result
179179

180180
def has_spf(self):
181+
"""Check if this domain has any Sender Policy Framework records."""
181182
if self.spf is not None:
182183
return len(self.spf) > 0
183184
return None
184185

185186
def has_dmarc(self):
187+
"""Check if this domain has a Domain-based Message Authentication, Reporting, and Conformance record."""
186188
if self.dmarc is not None:
187189
return len(self.dmarc) > 0
188190
return None
189191

190192
def add_mx_record(self, record):
193+
"""Add a mail server record for this domain."""
191194
if self.mx_records is None:
192195
self.mx_records = []
193196
self.mx_records.append(record)
@@ -198,30 +201,35 @@ def add_mx_record(self, record):
198201
self.mail_servers.append(record.exchange.to_text().rstrip(".").lower())
199202

200203
def parent_has_dmarc(self):
204+
"""Check if a domain or its parent has a Domain-based Message Authentication, Reporting, and Conformance record."""
201205
ans = self.has_dmarc()
202206
if self.base_domain:
203207
ans = self.base_domain.has_dmarc()
204208
return ans
205209

206210
def parent_dmarc_dnssec(self):
211+
"""Get this domain or its parent's DMARC DNSSEC information."""
207212
ans = self.dmarc_dnssec
208213
if self.base_domain:
209214
ans = self.base_domain.dmarc_dnssec
210215
return ans
211216

212217
def parent_valid_dmarc(self):
218+
"""Check if this domain or its parent have a valid DMARC record."""
213219
ans = self.valid_dmarc
214220
if self.base_domain:
215221
return self.base_domain.valid_dmarc
216222
return ans
217223

218224
def parent_dmarc_results(self):
225+
"""Get this domain or its parent's DMARC information."""
219226
ans = format_list(self.dmarc)
220227
if self.base_domain:
221228
ans = format_list(self.base_domain.dmarc)
222229
return ans
223230

224231
def get_dmarc_policy(self):
232+
"""Get this domain or its parent's DMARC policy."""
225233
ans = self.dmarc_policy
226234
# If the policy was never set, or isn't in the list of valid
227235
# policies, check the parents.
@@ -239,6 +247,7 @@ def get_dmarc_policy(self):
239247
return ans
240248

241249
def get_dmarc_subdomain_policy(self):
250+
"""Get this domain or its parent's DMARC subdomain policy."""
242251
ans = self.dmarc_subdomain_policy
243252
# If the policy was never set, or isn't in the list of valid
244253
# policies, check the parents.
@@ -250,41 +259,47 @@ def get_dmarc_subdomain_policy(self):
250259
return ans
251260

252261
def get_dmarc_pct(self):
262+
"""Get this domain or its parent's DMARC percentage information."""
253263
ans = self.dmarc_pct
254264
if not ans and self.base_domain:
255265
# Check the parents
256266
ans = self.base_domain.get_dmarc_pct()
257267
return ans
258268

259269
def get_dmarc_has_aggregate_uri(self):
270+
"""Get this domain or its parent's DMARC aggregate URI."""
260271
ans = self.dmarc_has_aggregate_uri
261272
# If there are no aggregate URIs then check the parents.
262273
if not ans and self.base_domain:
263274
ans = self.base_domain.get_dmarc_has_aggregate_uri()
264275
return ans
265276

266277
def get_dmarc_has_forensic_uri(self):
278+
"""Check if this domain or its parent have a DMARC forensic URI."""
267279
ans = self.dmarc_has_forensic_uri
268280
# If there are no forensic URIs then check the parents.
269281
if not ans and self.base_domain:
270282
ans = self.base_domain.get_dmarc_has_forensic_uri()
271283
return ans
272284

273285
def get_dmarc_aggregate_uris(self):
286+
"""Get this domain or its parent's DMARC aggregate URIs."""
274287
ans = self.dmarc_aggregate_uris
275288
# If there are no aggregate URIs then check the parents.
276289
if not ans and self.base_domain:
277290
ans = self.base_domain.get_dmarc_aggregate_uris()
278291
return ans
279292

280293
def get_dmarc_forensic_uris(self):
294+
"""Get this domain or its parent's DMARC forensic URIs."""
281295
ans = self.dmarc_forensic_uris
282296
# If there are no forensic URIs then check the parents.
283297
if not ans and self.base_domain:
284298
ans = self.base_domain.get_dmarc_forensic_uris()
285299
return ans
286300

287301
def generate_results(self):
302+
"""Generate the results for this domain."""
288303
if len(self.starttls_results.keys()) == 0:
289304
domain_supports_smtp = None
290305
domain_supports_starttls = None

src/trustymail/trustymail.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Functions to check a domain's configuration for trustworthy mail."""
2+
13
# Standard Python Libraries
24
from collections import OrderedDict
35
import csv
@@ -27,6 +29,7 @@
2729

2830

2931
def domain_list_from_url(url):
32+
"""Get a list of domains from a provided URL."""
3033
if not url:
3134
return []
3235

@@ -38,6 +41,7 @@ def domain_list_from_url(url):
3841

3942

4043
def domain_list_from_csv(csv_file):
44+
"""Get a list of domains from a provided CSV file."""
4145
domain_list = list(csv.reader(csv_file, delimiter=","))
4246

4347
# Check the headers for the word domain - use that column.
@@ -61,7 +65,9 @@ def domain_list_from_csv(csv_file):
6165

6266

6367
def check_dnssec(domain, domain_name, record_type):
64-
"""Checks whether the domain has a record of type that is protected
68+
"""Test to see if a DNSSEC record is valid and correct.
69+
70+
Checks a domain for DNSSEC whether the domain has a record of type that is protected
6571
by DNSSEC or NXDOMAIN or NoAnswer that is protected by DNSSEC.
6672
6773
TODO: Probably does not follow redirects (CNAMEs). Should work on
@@ -82,6 +88,7 @@ def check_dnssec(domain, domain_name, record_type):
8288

8389

8490
def mx_scan(resolver, domain):
91+
"""Scan a domain to see if it has any mail servers."""
8592
try:
8693
if domain.mx_records is None:
8794
domain.mx_records = []
@@ -426,9 +433,10 @@ def get_spf_record_text(resolver, domain_name, domain, follow_redirect=False):
426433

427434

428435
def spf_scan(resolver, domain):
429-
"""Scan a domain to see if it supports SPF. If the domain has an SPF
430-
record, verify that it properly handles mail sent from an IP known
431-
not to be listed in an MX record for ANY domain.
436+
"""Scan a domain to see if it supports SPF.
437+
438+
If the domain has an SPF record, verify that it properly handles mail sent from
439+
an IP known not to be listed in an MX record for ANY domain.
432440
433441
Parameters
434442
----------
@@ -460,7 +468,7 @@ def spf_scan(resolver, domain):
460468

461469
def parse_dmarc_report_uri(uri):
462470
"""
463-
Parses a DMARC Reporting (i.e. ``rua``/``ruf)`` URI
471+
Parse a DMARC Reporting (i.e. ``rua``/``ruf)`` URI.
464472
465473
Notes
466474
-----
@@ -492,6 +500,7 @@ def parse_dmarc_report_uri(uri):
492500

493501

494502
def dmarc_scan(resolver, domain):
503+
"""Scan a domain to see if it supports DMARC."""
495504
# dmarc records are kept in TXT records for _dmarc.domain_name.
496505
try:
497506
if domain.dmarc is None:
@@ -773,6 +782,7 @@ def dmarc_scan(resolver, domain):
773782

774783

775784
def find_host_from_ip(resolver, ip_addr):
785+
"""Find the host name for a given IP address."""
776786
# Use TCP, since we care about the content and correctness of the records
777787
# more than whether their records fit in a single UDP packet.
778788
hostname, _ = resolver.query(dns.reversename.from_address(ip_addr), "PTR", tcp=True)
@@ -789,6 +799,7 @@ def scan(
789799
scan_types,
790800
dns_hostnames,
791801
):
802+
"""Parse a domain's DNS information for mail related records."""
792803
#
793804
# Configure the dnspython library
794805
#
@@ -878,9 +889,10 @@ def scan(
878889

879890

880891
def handle_error(prefix, domain, error, syntax_error=False):
881-
"""Handle an error by logging via the Python logging library and
882-
recording it in the debug_info or syntax_error members of the
883-
trustymail.Domain object.
892+
"""Handle the provided error by logging a message and storing it in the Domain object.
893+
894+
Logging is performed via the Python logging library and recording it in the
895+
debug_info or syntax_error members of the trustymail.Domain object.
884896
885897
Since the "Debug Info" and "Syntax Error" fields in the CSV output
886898
of trustymail come directly from the debug_info and syntax_error
@@ -946,11 +958,12 @@ def handle_error(prefix, domain, error, syntax_error=False):
946958

947959

948960
def handle_syntax_error(prefix, domain, error):
949-
"""Convenience method for handle_error"""
961+
"""Handle a syntax error by passing it to handle_error()."""
950962
handle_error(prefix, domain, error, syntax_error=True)
951963

952964

953965
def generate_csv(domains, file_name):
966+
"""Generate a CSV file with the given domain information."""
954967
with open(file_name, "w", encoding="utf-8", newline="\n") as output_file:
955968
writer = csv.DictWriter(
956969
output_file, fieldnames=domains[0].generate_results().keys()
@@ -965,6 +978,7 @@ def generate_csv(domains, file_name):
965978

966979

967980
def generate_json(domains):
981+
"""Generate a JSON string with the given domain information."""
968982
output = []
969983
for domain in domains:
970984
output.append(domain.generate_results())
@@ -974,6 +988,7 @@ def generate_json(domains):
974988

975989
# Taken from pshtt to keep formatting similar
976990
def format_datetime(obj):
991+
"""Format the provided datetime information."""
977992
if isinstance(obj, datetime.date):
978993
return obj.isoformat()
979994
elif isinstance(obj, str):
@@ -983,7 +998,7 @@ def format_datetime(obj):
983998

984999

9851000
def remove_quotes(txt_record):
986-
"""Remove double quotes and contatenate strings in a DNS TXT record
1001+
"""Remove double quotes and contatenate strings in a DNS TXT record.
9871002
9881003
A DNS TXT record can contain multiple double-quoted strings, and
9891004
in that case the client has to remove the quotes and concatenate the

tests/test_trustymail.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
"""Tests for the trustymail module."""
12
# Standard Python Libraries
23
import unittest
34

45

56
class TestLiveliness(unittest.TestCase):
7+
"""Test the liveliness of a domain."""
8+
69
def test_domain_list_parsing(self):
10+
"""Test that a domain list is correctly parsed."""
711
pass

0 commit comments

Comments
 (0)