Domain-based Message Authentication, Reporting, and Conformance
249 | (DMARC) is a scalable mechanism by which a mail-originating
250 | organization can express domain-level policies and preferences for
251 | message validation, disposition, and reporting, that a mail-receiving
252 | organization can use to improve mail handling.
253 |
What is DMARC aiming to achieve?
254 |
Allows senders and receivers to improve and monitor
255 | protection of their domain from fraudulent email by building
256 | on and adding reporting to the widely deployed SPF and DKIM
257 | protocols.
258 |
How do aggregate feedback reports help?
259 |
Feedback reports provide a mechanism to monitor and
260 | improve mail handling.
261 |
262 |
How does django-dmarc make implementing DMARC easier?
263 |
Collecting feedback reports is automated, allowing easier
264 | filtering, with an option to download and use the data in a
265 | spreadsheet.
266 |
Can I get detailed information about failures?
267 |
Yes, set the ruf attribute and a detailed report will be
268 | sent for each failure. See the
269 | RFC for details.
Domain-based Message Authentication, Reporting, and Conformance
285 | (DMARC) is a scalable mechanism by which a mail-originating
286 | organization can express domain-level policies and preferences for
287 | message validation, disposition, and reporting, that a mail-receiving
288 | organization can use to improve mail handling.
289 |
290 |
adkim
291 |
Indicates whether
292 | strict or relaxed DKIM Identifier Alignment mode is required by
293 | the Domain Owner. Valid values are as follows:
294 |
r: relaxed mode (default)
295 |
s: strict mode
296 |
AFRF
297 |
Authentication Failure Reporting Format
298 |
aspf
299 |
Indicates whether
300 | strict or relaxed SPF Identifier Alignment mode is required by the
301 | Domain Owner. Valid values are as follows:
302 |
r: relaxed mode (default)
303 |
s: strict mode
304 |
Authenticated Identifiers
305 |
Domain-level identifiers that are
306 | validated using authentication technologies are referred to as
307 | "Authenticated Identifiers".
308 |
Author Domain
309 |
The domain name of the apparent author, as extracted
310 | from the RFC5322 From field.
311 |
312 |
DomainKeys Identified Mail (DKIM)
313 |
DKIM is an email authentication method designed to detect
314 | email spoofing. It allows the receiver to check that an email
315 | claimed to come from a specific domain was indeed authorized
316 | by the owner of that domain. It is intended to prevent forged
317 | sender addresses in emails, a technique often used in phishing
318 | and email spam.
319 |
DMARC Policy Record
320 |
Domain Owner DMARC preferences are stored as DNS TXT records in
321 | subdomains named "_dmarc". For example, the Domain Owner of
322 | "example.com" would post DMARC preferences in a TXT record at
323 | "_dmarc.example.com". Similarly, a Mail Receiver wishing to query
324 | for DMARC preferences regarding mail with an RFC5322.From domain of
325 | "example.com" would issue a TXT query to the DNS for the subdomain of
326 | "_dmarc.example.com".
327 |
Domain Owner
328 |
An entity or organization that owns a DNS domain. The
329 | term "owns" here indicates that the entity or organization being
330 | referenced holds the registration of that DNS domain. Domain
331 | Owners range from complex, globally distributed organizations, to
332 | service providers working on behalf of non-technical clients, to
333 | individuals responsible for maintaining personal domains. This
334 | specification uses this term as analogous to an Administrative
335 | Management Domain as defined in [EMAIL-ARCH]. It can also refer
336 | to delegates, such as Report Receivers, when those are outside of
337 | their immediate management domain.
338 |
339 |
fo
340 |
Failure reporting options.
341 |
Provides requested options for generation of failure reports.
342 | Report generators MAY choose to adhere to the requested options.
343 | This tag's content MUST be ignored if a "ruf" tag (below) is not
344 | also specified. The value of this tag is a colon-separated list
345 | of characters that indicate failure reporting options as follows:
346 |
0: Generate a DMARC failure report if all underlying
347 | authentication mechanisms fail to produce an aligned "pass"
348 | result. (default)
349 |
1: Generate a DMARC failure report if any underlying
350 | authentication mechanism produced something other than an
351 | aligned "pass" result.
352 |
d: Generate a DKIM failure report if the message had a signature
353 | that failed evaluation, regardless of its alignment.
354 |
s: Generate an SPF failure report if the message failed SPF
355 | evaluation, regardless of its alignment.
356 |
357 |
Identifier Alignment
358 |
When the domain in the RFC5322.From address
359 | matches a domain validated by SPF or DKIM (or both), it has
360 | Identifier Alignment.
361 |
362 |
Mail Receiver
363 |
The entity or organization that receives and
364 | processes email. Mail Receivers operate one or more Internet-
365 | facing Mail Transport Agents (MTAs).
366 |
367 |
Organizational Domain
368 |
The domain that was registered with a domain
369 | name registrar. In the absence of more accurate methods,
370 | heuristics are used to determine this, since it is not always the
371 | case that the registered domain name is simply a top-level DNS
372 | domain plus one component (e.g., "example.com", where "com" is a
373 | top-level domain).
374 |
375 |
p
376 |
Requested Mail Receiver policy.
377 | Indicates the policy to be enacted by the Receiver at
378 | the request of the Domain Owner. Policy applies to the domain
379 | queried and to subdomains, unless subdomain policy is explicitly
380 | described using the "sp" tag. This tag is mandatory for policy
381 | records only, but not for third-party reporting records.
382 | Possible values are as follows:
383 |
none: The Domain Owner requests no specific action be taken
384 | regarding delivery of messages.
385 |
quarantine: The Domain Owner wishes to have email that fails the
386 | DMARC mechanism check be treated by Mail Receivers as
387 | suspicious. Depending on the capabilities of the Mail
388 | Receiver, this can mean "place into spam folder", "scrutinize
389 | with additional intensity", and/or "flag as suspicious".
390 |
reject: The Domain Owner wishes for Mail Receivers to reject
391 | email that fails the DMARC mechanism check. Rejection SHOULD
392 | occur during the SMTP transaction. See Section 10.3 for some
393 | discussion of SMTP rejection methods and their implications.
394 |
395 |
pct
396 |
Percentage of messages from the Domain Owner's
397 | mail stream to which the DMARC policy is to be applied. However,
398 | this MUST NOT be applied to the DMARC-generated reports, all of
399 | which must be sent and received unhindered. The purpose of the
400 | "pct" tag is to allow Domain Owners to enact a slow rollout
401 | enforcement of the DMARC mechanism. The prospect of "all or
402 | nothing" is recognized as preventing many organizations from
403 | experimenting with strong authentication-based mechanisms.
404 |
405 |
Report Receiver
406 |
An operator that receives reports from another
407 | operator implementing the reporting mechanism described in this
408 | document. Such an operator might be receiving reports about its
409 | own messages, or reports about messages related to another
410 | operator. This term applies collectively to the system components
411 | that receive and process these reports and the organizations that
412 | operate them.
413 |
rf
414 |
Format to be used for message-specific failure reports.
415 | The value of this tag is a list of one or more report formats as
416 | requested by the Domain Owner to be used when a message fails both
417 | [SPF] and [DKIM] tests to report details of the individual
418 | failure. For this version, only "afrf" (the auth-failure report
419 | type defined in [AFRF]) is presently supported.
420 |
ri
421 |
Interval requested between aggregate reports.
422 | The default is 86400 (one day). Indicates a
423 | request to Receivers to generate aggregate reports separated by no
424 | more than the requested number of seconds. DMARC implementations
425 | MUST be able to provide daily reports and SHOULD be able to
426 | provide hourly reports when requested. However, anything other
427 | than a daily report is understood to be accommodated on a best-
428 | effort basis.
429 |
rua
430 |
Addresses to which aggregate feedback is to be sent.
431 |
ruf
432 |
Addresses to which message-specific failure information is to
433 | be reported. If present, the Domain Owner is requesting Mail
434 | Receivers to send detailed failure reports about messages that
435 | fail the DMARC evaluation in specific ways (see the "fo" tag).
436 |
437 |
Sender Policy Framework (SPF)
438 |
Sender Policy Framework is a simple email-validation system
439 | designed to detect email spoofing by providing a mechanism to
440 | allow receiving mail exchangers to check that incoming mail from
441 | a domain comes from a host authorized by that domain's
442 | administrators.
443 |
sp
444 |
Requested Mail Receiver policy for all subdomains.
445 | Indicates the policy to be enacted by the Receiver at
446 | the request of the Domain Owner. It applies only to subdomains of
447 | the domain queried and not to the domain itself. Its syntax is
448 | identical to that of the "p" tag. If absent, the
449 | policy specified by the "p" tag MUST be applied for subdomains.
450 | Note that "sp" will be ignored for DMARC records published on
451 | subdomains of Organizational Domains due to the effect of the
452 | DMARC policy discovery mechanism
453 |
454 |
v
455 |
Version. Identifies the record retrieved
456 | as a DMARC record. It MUST have the value of "DMARC1".
17 | none
18 | 100
19 |
20 |
21 |
22 | 80.229.143.200
23 | 1
24 |
25 | none
26 | pass
27 | pass
28 |
29 |
30 |
31 | p-o.co.uk
32 |
33 |
34 |
35 | p-o.co.uk
36 | pass
37 |
38 |
39 | p-o.co.uk
40 | pass
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/dmarc/urls.py:
--------------------------------------------------------------------------------
1 | #----------------------------------------------------------------------
2 | # Copyright (c) 2015-2021, Persistent Objects Ltd https://p-o.co.uk/
3 | #
4 | # License: BSD
5 | #----------------------------------------------------------------------
6 | """
7 | DMARC urls
8 | http://dmarc.org/resources/specification/
9 | """
10 | from django.conf.urls import url
11 | from dmarc import views
12 |
13 | app_name = 'dmarc'
14 | urlpatterns = [
15 | url("^report/$", views.dmarc_report, name='dmarc_report'),
16 | url("^report/csv/$", views.dmarc_csv, name='dmarc_csv'),
17 | url("^report/json/$", views.dmarc_json, name='dmarc_json'),
18 | ]
19 |
--------------------------------------------------------------------------------
/dmarc/views.py:
--------------------------------------------------------------------------------
1 | #----------------------------------------------------------------------
2 | # Copyright (c) 2015-2021, Persistent Objects Ltd https://p-o.co.uk/
3 | #
4 | # License: BSD
5 | #----------------------------------------------------------------------
6 | """
7 | DMARC views
8 | https://dmarc.org/resources/specification/
9 | """
10 | import csv
11 | import datetime
12 |
13 | from django.contrib.admin.views.decorators import staff_member_required
14 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
15 | from django.db import connection
16 | from django.http import JsonResponse, StreamingHttpResponse
17 | from django.shortcuts import render
18 |
19 | from dmarc.models import Report
20 |
21 | class Echo(object):
22 | """An object that implements just the write method of the file-like
23 | interface for csv.writer.
24 | """
25 | def write(self, value):
26 | """Write the value by returning it, instead of storing in a buffer."""
27 | return value
28 |
29 | def _sql_cursor(request_args):
30 | """Returns a cursor according to users request"""
31 | sql_where = []
32 | sql_orderby = []
33 | sql_params = []
34 | if 'dmarc_date_from' in request_args:
35 | val = request_args['dmarc_date_from']
36 | try:
37 | val = datetime.datetime.strptime(val, '%Y-%m-%d')
38 | except:
39 | val = datetime.date.today()
40 | sql_where.append('dmarc_report.date_begin >= %s')
41 | sql_params.append(val)
42 | if 'dmarc_date_to' in request_args:
43 | val = request_args['dmarc_date_to']
44 | try:
45 | val = datetime.datetime.strptime(val, '%Y-%m-%d')
46 | except:
47 | val = datetime.date.today()
48 | td = datetime.timedelta(days=1)
49 | val = val + td
50 | sql_where.append('dmarc_report.date_end < %s')
51 | sql_params.append(val)
52 | if 'dmarc_disposition' in request_args and request_args['dmarc_disposition']:
53 | val = request_args['dmarc_disposition']
54 | sql_where.append('dmarc_record.policyevaluated_disposition = %s')
55 | sql_params.append(val)
56 | if 'dmarc_onlyerror' in request_args:
57 | s = '('
58 | s = s + "dmarc_record.policyevaluated_dkim = 'fail'"
59 | s = s + " OR "
60 | s = s + "dmarc_record.policyevaluated_spf = 'fail'"
61 | s = s + ')'
62 | sql_where.append(s)
63 | if 'dmarc_filter' in request_args and request_args['dmarc_filter']:
64 | val = request_args['dmarc_filter'] + '%'
65 | s = '('
66 | s = s + "lower(dmarc_reporter.org_name) LIKE lower(%s)"
67 | s = s + " OR "
68 | s = s + "dmarc_record.source_ip LIKE %s"
69 | s = s + ')'
70 | sql_where.append(s)
71 | sql_params.append(val)
72 | sql_params.append(val)
73 |
74 | sql = """
75 | SELECT
76 | dmarc_reporter.org_name,
77 | dmarc_reporter.email,
78 | dmarc_report.date_begin,
79 | dmarc_report.date_end,
80 | dmarc_report.policy_domain,
81 | dmarc_report.policy_adkim,
82 | dmarc_report.policy_aspf,
83 | dmarc_report.policy_p,
84 | dmarc_report.policy_sp,
85 | dmarc_report.policy_pct,
86 | dmarc_report.report_id,
87 | dmarc_record.source_ip,
88 | dmarc_record.recordcount,
89 | dmarc_record.policyevaluated_disposition,
90 | dmarc_record.policyevaluated_dkim,
91 | dmarc_record.policyevaluated_spf,
92 | dmarc_record.policyevaluated_reasontype,
93 | dmarc_record.policyevaluated_reasoncomment,
94 | dmarc_record.identifier_headerfrom,
95 | spf_dmarc_result.record_type AS spf_record_type,
96 | spf_dmarc_result.domain AS spf_domain,
97 | spf_dmarc_result.result AS spf_result,
98 | dkim_dmarc_result.record_type AS dkim_record_type,
99 | dkim_dmarc_result.domain AS dkim_domain,
100 | dkim_dmarc_result.result AS dkim_result
101 | FROM dmarc_reporter
102 | INNER JOIN dmarc_report
103 | ON dmarc_report.reporter_id = dmarc_reporter.id
104 | INNER JOIN dmarc_record
105 | ON dmarc_record.report_id = dmarc_report.id
106 | LEFT OUTER JOIN dmarc_result AS spf_dmarc_result
107 | ON spf_dmarc_result.record_id = dmarc_record.id
108 | AND spf_dmarc_result.record_type = 'spf'
109 | LEFT OUTER JOIN dmarc_result AS dkim_dmarc_result
110 | ON dkim_dmarc_result.record_id = dmarc_record.id
111 | AND dkim_dmarc_result.record_type = 'dkim'
112 | """
113 |
114 | if sql_where:
115 | sql = sql + " WHERE " + "\nAND ".join(sql_where)
116 |
117 | sql_orderby.append('LOWER(dmarc_reporter.org_name)')
118 | sql_orderby.append('dmarc_report.date_begin')
119 | sql_orderby.append('dmarc_record.source_ip')
120 | sql = sql + "\nORDER BY " + ", ".join(sql_orderby)
121 |
122 | cursor = connection.cursor()
123 | cursor.execute(sql, sql_params)
124 |
125 | return cursor
126 |
127 | @staff_member_required
128 | def dmarc_index(request):
129 |
130 | context = {
131 | "reports": 'TODO',
132 | }
133 | return render(request, 'dmarc/report.html', context)
134 |
135 | @staff_member_required
136 | def dmarc_report(request):
137 | report_list = Report.objects.select_related(
138 | 'reporter',
139 | ).prefetch_related(
140 | 'records__results'
141 | ).order_by('-date_begin', 'reporter__org_name').all()
142 | paginator = Paginator(report_list, 2)
143 |
144 | page = request.GET.get('page')
145 | try:
146 | reports = paginator.page(page)
147 | except PageNotAnInteger:
148 | # If page is not an integer, deliver first page.
149 | reports = paginator.page(1)
150 | except EmptyPage:
151 | # If page is out of range (e.g. 9999), deliver last page of results.
152 | reports = paginator.page(paginator.num_pages)
153 |
154 | context = {
155 | "reports": reports,
156 | }
157 | return render(request, 'dmarc/report.html', context)
158 |
159 | @staff_member_required
160 | def dmarc_csv(request):
161 | """Export dmarc data as a csv"""
162 | # Inspired by https://code.djangoproject.com/ticket/21179
163 | def stream():
164 | """Generator function to yield cursor rows."""
165 | pseudo_buffer = Echo()
166 | writer = csv.writer(pseudo_buffer)
167 | columns = True
168 | for row in cursor.fetchall():
169 | data = ''
170 | if columns:
171 | # Write the columns if this is the first row
172 | columns = [col[0] for col in cursor.description]
173 | data = writer.writerow(columns)
174 | columns = False
175 | data = data + writer.writerow(row)
176 | yield data
177 |
178 | dt = datetime.datetime.now()
179 | cd = 'attachment; filename="dmarc-{}.csv"'.format(dt.strftime('%Y-%m-%d-%H%M%S'))
180 |
181 | cursor = _sql_cursor(request.GET)
182 |
183 | response = StreamingHttpResponse(stream(), content_type="text/csv")
184 | response['Content-Disposition'] = cd
185 |
186 | return response
187 |
188 | @staff_member_required
189 | def dmarc_json(request):
190 | """Export dmarc data as json"""
191 |
192 | cursor = _sql_cursor(request.GET)
193 |
194 | data = JsonResponse(cursor.fetchall(), safe=False)
195 | return data
196 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
23 |
24 | help:
25 | @echo "Please use \`make ' where is one of"
26 | @echo " html to make standalone HTML files"
27 | @echo " dirhtml to make HTML files named index.html in directories"
28 | @echo " singlehtml to make a single large HTML file"
29 | @echo " pickle to make pickle files"
30 | @echo " json to make JSON files"
31 | @echo " htmlhelp to make HTML files and a HTML help project"
32 | @echo " qthelp to make HTML files and a qthelp project"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 |
49 | clean:
50 | rm -rf $(BUILDDIR)/*
51 |
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | dirhtml:
58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
59 | @echo
60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
61 |
62 | singlehtml:
63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
64 | @echo
65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
66 |
67 | pickle:
68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
69 | @echo
70 | @echo "Build finished; now you can process the pickle files."
71 |
72 | json:
73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
74 | @echo
75 | @echo "Build finished; now you can process the JSON files."
76 |
77 | htmlhelp:
78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
79 | @echo
80 | @echo "Build finished; now you can run HTML Help Workshop with the" \
81 | ".hhp project file in $(BUILDDIR)/htmlhelp."
82 |
83 | qthelp:
84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
85 | @echo
86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoDmarc.qhcp"
89 | @echo "To view the help file:"
90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoDmarc.qhc"
91 |
92 | devhelp:
93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
94 | @echo
95 | @echo "Build finished."
96 | @echo "To view the help file:"
97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoDmarc"
98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoDmarc"
99 | @echo "# devhelp"
100 |
101 | epub:
102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
103 | @echo
104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
105 |
106 | latex:
107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
108 | @echo
109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
111 | "(use \`make latexpdf' here to do that automatically)."
112 |
113 | latexpdf:
114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
115 | @echo "Running LaTeX files through pdflatex..."
116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
118 |
119 | latexpdfja:
120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121 | @echo "Running LaTeX files through platex and dvipdfmx..."
122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
124 |
125 | text:
126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
127 | @echo
128 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
129 |
130 | man:
131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
132 | @echo
133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
134 |
135 | texinfo:
136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
137 | @echo
138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
139 | @echo "Run \`make' in that directory to run these through makeinfo" \
140 | "(use \`make info' here to do that automatically)."
141 |
142 | info:
143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
144 | @echo "Running Texinfo files through makeinfo..."
145 | make -C $(BUILDDIR)/texinfo info
146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
147 |
148 | gettext:
149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
150 | @echo
151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
152 |
153 | changes:
154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
155 | @echo
156 | @echo "The overview file is in $(BUILDDIR)/changes."
157 |
158 | linkcheck:
159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
160 | @echo
161 | @echo "Link check complete; look for any errors in the above output " \
162 | "or in $(BUILDDIR)/linkcheck/output.txt."
163 |
164 | doctest:
165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
166 | @echo "Testing of doctests in the sources finished, look at the " \
167 | "results in $(BUILDDIR)/doctest/output.txt."
168 |
169 | xml:
170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
171 | @echo
172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
173 |
174 | pseudoxml:
175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
176 | @echo
177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
178 |
--------------------------------------------------------------------------------
/docs/README.rst:
--------------------------------------------------------------------------------
1 | =============
2 | Documentation
3 | =============
4 |
5 | **Managing DMARC aggregate and feedback reports**
6 |
7 | Designed to quickly and easily manage DMARC aggregate and feedback reports.
8 |
9 | Description
10 | ===========
11 |
12 | This Django DMARC project aims to ease implementating DMARC
13 | "Domain-based Message Authentication, Reporting & Conformance" and
14 | ongoing monitoring by importing aggregate and feedback reports about messages
15 | that pass and/or fail DMARC evaluation into a more easily digested format.
16 |
17 | Perhaps one of the main reasons DMARC is gaining traction amongst
18 | organisations of all sizes is a desire to protect their people, brand and
19 | reputation.
20 | By defining and implementing a DMARC policy, an organization can help combat
21 | phishing, protect users and their reputation.
22 |
23 | At beta stage, the application is stable, with most efforts on improving
24 | usability and documentation.
25 |
26 | Choosing Django was an easy choice as it offers an easily built import
27 | mechanism and transformation from xml to database through to presentation.
28 |
29 | Although it has options for importing either xml or email files, zero
30 | maintenance is achieved by fully automating import of feedback and reports.
31 |
32 | Quick start
33 | ===========
34 |
35 | 1. Install the app
36 |
37 | 2. Add "dmarc" to your INSTALLED_APPS setting::
38 |
39 | INSTALLED_APPS = (
40 | ...
41 | 'dmarc',
42 | )
43 |
44 | 3. Add dmarc.urls to your urls::
45 |
46 | from dmarc import urls as dmarc_urls
47 |
48 | urlpatterns = [
49 | ...
50 | url(r"^dmarc/", include(dmarc_urls)),
51 | ]
52 |
53 | 4. Run 'python manage.py migrate' to create the database models.
54 |
55 | 5. Import a report with::
56 |
57 | python manage.py importdmarcreport --email
58 |
59 | 6. See your aggregated feedback reports from the Admin page at admin/dmarc
60 |
61 | Usage
62 | =====
63 | python manage.py importdmarcreport --email
64 |
65 | You can choose to import an xml or email file, alternatively with "--email -"
66 | you can pipe an email and it will do the right thing.
67 |
68 | Installation
69 | ============
70 |
71 | Install the app
72 |
73 | Configuration
74 | -------------
75 |
76 | Add "dmarc" to your INSTALLED_APPS setting::
77 |
78 | INSTALLED_APPS = (
79 | ...
80 | 'dmarc',
81 | )
82 |
83 | Add dmarc.urls to your urls::
84 |
85 | from dmarc import urls as dmarc_urls
86 |
87 | urlpatterns = [
88 | ...
89 | url(r"^dmarc/", include(dmarc_urls)),
90 | ]
91 |
92 | DMARC reports are namespaced so if you're using django version 1.8 you will
93 | need to add the namespace 'dmarc'::
94 |
95 | urlpatterns = [
96 | ...
97 | url(r"^dmarc/", include(dmarc_urls, namespace='dmarc')),
98 | ]
99 |
100 | Install tables
101 | --------------
102 |
103 | Run 'python manage.py migrate' to create the database tables.
104 |
105 | Import feedback report
106 | ----------------------
107 |
108 | Import an email DMARC aggregate report with::
109 |
110 | python manage.py importdmarcreport --email
111 |
112 | Alternatively the xml report can be imported with::
113 |
114 | python manage.py importdmarcreport --xml
115 |
116 | The process of importing DMARC aggregate reports can be fully automated. At
117 | Persistent Objects we use Exim and the configuration couldn't be easier.
118 |
119 | Router::
120 |
121 | dmarcreports:
122 | driver = accept
123 | condition = ${if eq{$local_part}{dmarc_report}}
124 | transport = trans_dmarcreports
125 |
126 | Transport::
127 |
128 | trans_dmarcreports:
129 | driver = pipe
130 | command = "/usr/local/bin/python2.7 /path/to/manage.py importdmarcreport --email -"
131 | freeze_exec_fail = true
132 | return_fail_output = true
133 |
134 | Congratulations, you have django-dmarc installed and ready to import DMARC
135 | aggregate feedback reports and start implementing DMARC and protecting your
136 | emails.
137 |
138 | DMARC reporting
139 | ===============
140 |
141 | Aggregated feedback reports are available from the Admin page at admin/dmarc.
142 |
143 | .. image:: https://p-o.co.uk/media/content/dmarc-index.png
144 | :alt: Django Administration showing this DMARC application
145 |
146 | From the DMARC dashboard at 'Site administration/DMARC' where the intention is
147 | to highlight a summary of recent reports, there is one report 'DMARC feedback
148 | reports' and is available to any user with staff members authorization.
149 |
150 | .. image:: https://p-o.co.uk/media/content/dmarc-dashboard.png
151 | :alt: DMARC dashboard
152 |
153 | This is an example report, it can also be downloaded as a csv file suitable
154 | for importing into your favourite spreadsheet.
155 |
156 | .. image:: https://p-o.co.uk/media/content/dmarc-report.png
157 | :alt: Example DMARC aggregate feedback report
158 |
159 | The report can be filtered by clicking on the filter and changing any of the
160 | reporting period, only showing errors/failures, disposition (quarantine,
161 | rejection or any) and by source ip address/reporting organisation.
162 |
163 | .. image:: https://p-o.co.uk/media/content/dmarc-reportfilter.png
164 | :alt: Example DMARC aggregate feedback report
165 |
166 | These reports can help ease any DMARC implementation.
167 |
168 | Maintenance
169 | ===========
170 |
171 | Although there is usually no need to remove old records, access to the report
172 | table is offered to allow for record deletion.
173 |
174 | Dependencies
175 | ============
176 |
177 | * `Django`_ 2.2+
178 |
179 | Resources
180 | =========
181 |
182 | * `DMARC`_
183 | * `Django`_
184 | * `Google gmail DMARC`_
185 | * `Download from PyPI`_
186 |
187 | Support
188 | =======
189 |
190 | To report a security issue, please send an email privately to
191 | `ahicks@p-o.co.uk`_. This gives us a chance to fix the issue and
192 | create an official release prior to the issue being made
193 | public.
194 |
195 | For general questions or comments, please contact `ahicks@p-o.co.uk`_.
196 |
197 | `Project website`_
198 |
199 | Communications are expected to conform to the `Django Code of Conduct`_.
200 |
201 | .. GENERAL LINKS
202 |
203 | .. _`Django`: https://djangoproject.com/
204 | .. _`Django Code of Conduct`: https://www.djangoproject.com/conduct/
205 | .. _`Python`: https://python.org/
206 | .. _`Persistent Objects Ltd`: https://p-o.co.uk/
207 | .. _`Project website`: https://p-o.co.uk/tech-articles/django-dmarc/
208 | .. _`DMARC`: https://dmarc.org/
209 | .. _`Google gmail DMARC`: https://support.google.com/a/answer/2466580
210 | .. _`Download from PyPI`: https://pypi.org/project/django-dmarc/
211 |
212 | .. PEOPLE WITH QUOTES
213 |
214 | .. _`Alan Hicks`: https://twitter.com/AlanHicksLondon
215 | .. _`ahicks@p-o.co.uk`: mailto:ahicks@p-o.co.uk?subject=django-dmarc+Security+Issue
216 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | Change Log
3 | ===========
4 |
5 | **Managing DMARC aggregate and feedback reports**
6 |
7 | Designed to quickly and easily manage DMARC aggregate and feedback reports.
8 |
9 | Change Log
10 | ==========
11 |
12 | .. include:: ../CHANGELOG
13 |
14 | Copyright
15 | =========
16 |
17 | Django DMARC and this documentation is
18 | Copyright (c) 2015-2021, Persistent Objects Ltd.
19 | All rights reserved.
20 |
21 | License
22 | =======
23 |
24 | This documentation is licensed under the Creative Commons Attribution 4.0
25 | International License. To view a copy of this license, visit
26 | http://creativecommons.org/licenses/by/4.0/.
27 |
28 | .. include:: ../LICENSE
29 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Django DMARC documentation build configuration file, created by
4 | # sphinx-quickstart on Mon May 12 11:13:16 2014.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import sys
16 | import os
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | ##sys.path.insert(0, os.path.abspath('.'))
22 |
23 | # -- General configuration ------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | #needs_sphinx = '1.0'
27 |
28 | # Add any Sphinx extension module names here, as strings. They can be
29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
30 | # ones.
31 | extensions = [
32 | 'sphinx.ext.autodoc',
33 | 'sphinx.ext.doctest',
34 | 'sphinx.ext.todo',
35 | 'sphinx.ext.coverage',
36 | ]
37 |
38 | # Add any paths that contain templates here, relative to this directory.
39 | templates_path = ['_templates']
40 |
41 | # The suffix of source filenames.
42 | source_suffix = '.rst'
43 |
44 | # The encoding of source files.
45 | #source_encoding = 'utf-8-sig'
46 |
47 | # The master toctree document.
48 | master_doc = 'index'
49 |
50 | # General information about the project.
51 | project = u'Django DMARC'
52 | copyright = u'2015-2021, Persistent Objects Ltd'
53 |
54 | # The version info for the project you're documenting, acts as replacement for
55 | # |version| and |release|, also used in various other places throughout the
56 | # built documents.
57 | #
58 | # The short X.Y version.
59 | version = '1.0'
60 | # The full version, including alpha/beta/rc tags.
61 | release = '1.0.1'
62 |
63 | # The language for content autogenerated by Sphinx. Refer to documentation
64 | # for a list of supported languages.
65 | #language = None
66 |
67 | # There are two options for replacing |today|: either, you set today to some
68 | # non-false value, then it is used:
69 | #today = ''
70 | # Else, today_fmt is used as the format for a strftime call.
71 | #today_fmt = '%B %d, %Y'
72 |
73 | # List of patterns, relative to source directory, that match files and
74 | # directories to ignore when looking for source files.
75 | exclude_patterns = ['_build']
76 |
77 | # The reST default role (used for this markup: `text`) to use for all
78 | # documents.
79 | #default_role = None
80 |
81 | # If true, '()' will be appended to :func: etc. cross-reference text.
82 | #add_function_parentheses = True
83 |
84 | # If true, the current module name will be prepended to all description
85 | # unit titles (such as .. function::).
86 | #add_module_names = True
87 |
88 | # If true, sectionauthor and moduleauthor directives will be shown in the
89 | # output. They are ignored by default.
90 | #show_authors = False
91 |
92 | # The name of the Pygments (syntax highlighting) style to use.
93 | pygments_style = 'sphinx'
94 |
95 | # A list of ignored prefixes for module index sorting.
96 | #modindex_common_prefix = []
97 |
98 | # If true, keep warnings as "system message" paragraphs in the built documents.
99 | #keep_warnings = False
100 |
101 |
102 | # -- Options for HTML output ----------------------------------------------
103 |
104 | # The theme to use for HTML and HTML Help pages. See the documentation for
105 | # a list of builtin themes.
106 | html_theme = 'default'
107 |
108 | # Theme options are theme-specific and customize the look and feel of a theme
109 | # further. For a list of options available for each theme, see the
110 | # documentation.
111 | #html_theme_options = {}
112 |
113 | # Add any paths that contain custom themes here, relative to this directory.
114 | #html_theme_path = []
115 |
116 | # The name for this set of Sphinx documents. If None, it defaults to
117 | # " v documentation".
118 | #html_title = None
119 |
120 | # A shorter title for the navigation bar. Default is the same as html_title.
121 | #html_short_title = None
122 |
123 | # The name of an image file (relative to this directory) to place at the top
124 | # of the sidebar.
125 | #html_logo = None
126 |
127 | # The name of an image file (within the static path) to use as favicon of the
128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
129 | # pixels large.
130 | #html_favicon = None
131 |
132 | # Add any paths that contain custom static files (such as style sheets) here,
133 | # relative to this directory. They are copied after the builtin static files,
134 | # so a file named "default.css" will overwrite the builtin "default.css".
135 | html_static_path = ['_static']
136 |
137 | # Add any extra paths that contain custom files (such as robots.txt or
138 | # .htaccess) here, relative to this directory. These files are copied
139 | # directly to the root of the documentation.
140 | #html_extra_path = []
141 |
142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
143 | # using the given strftime format.
144 | #html_last_updated_fmt = '%b %d, %Y'
145 |
146 | # If true, SmartyPants will be used to convert quotes and dashes to
147 | # typographically correct entities.
148 | #html_use_smartypants = True
149 |
150 | # Custom sidebar templates, maps document names to template names.
151 | #html_sidebars = {}
152 |
153 | # Additional templates that should be rendered to pages, maps page names to
154 | # template names.
155 | #html_additional_pages = {}
156 |
157 | # If false, no module index is generated.
158 | #html_domain_indices = True
159 |
160 | # If false, no index is generated.
161 | #html_use_index = True
162 |
163 | # If true, the index is split into individual pages for each letter.
164 | #html_split_index = False
165 |
166 | # If true, links to the reST sources are added to the pages.
167 | #html_show_sourcelink = True
168 |
169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
170 | #html_show_sphinx = True
171 |
172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
173 | #html_show_copyright = True
174 |
175 | # If true, an OpenSearch description file will be output, and all pages will
176 | # contain a tag referring to it. The value of this option must be the
177 | # base URL from which the finished HTML is served.
178 | #html_use_opensearch = ''
179 |
180 | # This is the file name suffix for HTML files (e.g. ".xhtml").
181 | #html_file_suffix = None
182 |
183 | # Output file base name for HTML help builder.
184 | htmlhelp_basename = 'DjangoDmarcdoc'
185 |
186 |
187 | # -- Options for LaTeX output ---------------------------------------------
188 |
189 | latex_elements = {
190 | # The paper size ('letterpaper' or 'a4paper').
191 | #'papersize': 'letterpaper',
192 |
193 | # The font size ('10pt', '11pt' or '12pt').
194 | #'pointsize': '10pt',
195 |
196 | # Additional stuff for the LaTeX preamble.
197 | #'preamble': '',
198 | }
199 |
200 | # Grouping the document tree into LaTeX files. List of tuples
201 | # (source start file, target name, title,
202 | # author, documentclass [howto, manual, or own class]).
203 | latex_documents = [
204 | ('index', 'DjangoDmarcdoc.tex', u'Django Dmarc Documentation',
205 | u'Alan Hicks', 'manual'),
206 | ]
207 |
208 | # The name of an image file (relative to this directory) to place at the top of
209 | # the title page.
210 | #latex_logo = None
211 |
212 | # For "manual" documents, if this is true, then toplevel headings are parts,
213 | # not chapters.
214 | #latex_use_parts = False
215 |
216 | # If true, show page references after internal links.
217 | #latex_show_pagerefs = False
218 |
219 | # If true, show URL addresses after external links.
220 | #latex_show_urls = False
221 |
222 | # Documents to append as an appendix to all manuals.
223 | #latex_appendices = []
224 |
225 | # If false, no module index is generated.
226 | #latex_domain_indices = True
227 |
228 |
229 | # -- Options for manual page output ---------------------------------------
230 |
231 | # One entry per manual page. List of tuples
232 | # (source start file, name, description, authors, manual section).
233 | man_pages = [
234 | ('index', 'django_dmarc', u'Django DMARC Documentation',
235 | [u'Alan Hicks'], 1)
236 | ]
237 |
238 | # If true, show URL addresses after external links.
239 | #man_show_urls = False
240 |
241 |
242 | # -- Options for Texinfo output -------------------------------------------
243 |
244 | # Grouping the document tree into Texinfo files. List of tuples
245 | # (source start file, target name, title, author,
246 | # dir menu entry, description, category)
247 | texinfo_documents = [
248 | ('index', 'django-dmarc', u'Django DMARC Documentation',
249 | u'Alan Hicks', 'django-dmarc',
250 | 'Making it easier to manage DMARC reports',
251 | 'Miscellaneous'),
252 | ]
253 |
254 | # Documents to append as an appendix to all manuals.
255 | #texinfo_appendices = []
256 |
257 | # If false, no module index is generated.
258 | #texinfo_domain_indices = True
259 |
260 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
261 | #texinfo_show_urls = 'footnote'
262 |
263 | # If true, do not generate a @detailmenu in the "Top" node's menu.
264 | #texinfo_no_detailmenu = False
265 |
--------------------------------------------------------------------------------
/docs/images/dmarc-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alan-hicks/django-dmarc/c5ad9911f7c3698eeb6ea424fb9e494170e6e96b/docs/images/dmarc-dashboard.png
--------------------------------------------------------------------------------
/docs/images/dmarc-index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alan-hicks/django-dmarc/c5ad9911f7c3698eeb6ea424fb9e494170e6e96b/docs/images/dmarc-index.png
--------------------------------------------------------------------------------
/docs/images/dmarc-report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alan-hicks/django-dmarc/c5ad9911f7c3698eeb6ea424fb9e494170e6e96b/docs/images/dmarc-report.png
--------------------------------------------------------------------------------
/docs/images/dmarc-reportfilter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alan-hicks/django-dmarc/c5ad9911f7c3698eeb6ea424fb9e494170e6e96b/docs/images/dmarc-reportfilter.png
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. Django DMARC documentation master file, created by
2 | sphinx-quickstart on Mon May 12 11:13:16 2014.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | ============
7 | Django DMARC
8 | ============
9 |
10 | **Managing DMARC aggregate and feedback reports**
11 |
12 | Designed to quickly and easily manage DMARC aggregate and feedback reports.
13 |
14 | Contents
15 | ========
16 |
17 | .. toctree::
18 | :maxdepth: 1
19 |
20 | README
21 | changelog
22 |
23 | Description
24 | ===========
25 |
26 | This Django DMARC project aims to ease implementating DMARC
27 | "Domain-based Message Authentication, Reporting & Conformance" and
28 | ongoing monitoring by importing aggregate and feedback reports about messages
29 | that pass and/or fail DMARC evaluation into a more easily digested format.
30 |
31 | Perhaps one of the main reasons DMARC is gaining traction amongst
32 | organisations of all sizes is a desire to protect their people, brand and
33 | reputation.
34 | By defining and implementing a DMARC policy, an organization can help combat
35 | phishing, protect users and their reputation.
36 |
37 | This project is stable, with most efforts on improving usability and
38 | documentation.
39 |
40 | Choosing Django was an easy choice as it offers an easily built import
41 | mechanism and transformation from xml to database through to presentation.
42 |
43 | Although there are options for importing either xml or email files, zero
44 | maintenance is achieved by fully automating import of feedback and reports.
45 |
46 | Copyright
47 | =========
48 |
49 | Django DMARC and this documentation is
50 | Copyright (c) 2015-2021, Persistent Objects Ltd.
51 | All rights reserved.
52 |
53 | Contributors
54 | ============
55 |
56 | This list is not complete and not in any useful order, but I would
57 | like to thank everybody who contributed in any way, with code, hints,
58 | bug reports, ideas, moral support, endorsement, or even complaints...
59 | You have made django-dmarc what it is today.
60 |
61 | | Thank you,
62 | | Alan Hicks
63 |
64 | .. include:: ../AUTHORS
65 |
66 | License
67 | =======
68 |
69 | This documentation is licensed under the Creative Commons Attribution 4.0
70 | International License. To view a copy of this license, visit
71 | https://creativecommons.org/licenses/by/4.0/.
72 |
73 | The software is licensed under the BSD two clause license.
74 |
75 | .. include:: ../LICENSE
76 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """Managing DMARC aggregate and feedback reports
2 | """
3 | from setuptools import setup
4 | from codecs import open
5 | from os import path
6 |
7 | here = path.abspath(path.dirname(__file__))
8 | # Get the long description from the README file
9 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
10 | long_description = f.read()
11 |
12 | setup(
13 | name='django-dmarc',
14 | version='1.0.1',
15 | packages=['dmarc'],
16 | include_package_data=True,
17 | license='BSD',
18 | description='Managing DMARC aggregate and feedback reports',
19 | long_description=long_description,
20 | long_description_content_type='text/x-rst',
21 | url='https://github.com/alan-hicks/django-dmarc',
22 | project_urls={
23 | "About": "https://p-o.co.uk/tech-articles/django-dmarc/",
24 | },
25 | download_url='https://pypi.python.org/pypi/django-dmarc',
26 | author='Alan Hicks',
27 | author_email='ahicks@p-o.co.uk',
28 | requires=['django'],
29 | classifiers=[
30 | 'Development Status :: 5 - Production/Stable',
31 | 'Environment :: Web Environment',
32 | 'Framework :: Django',
33 | 'Framework :: Django :: 2.2',
34 | 'Intended Audience :: Developers',
35 | 'License :: OSI Approved :: BSD License',
36 | 'Natural Language :: English',
37 | 'Operating System :: OS Independent',
38 | 'Programming Language :: Python :: 3',
39 | 'Programming Language :: Python :: 3.7',
40 | 'Programming Language :: Python :: 3.8',
41 | 'Topic :: Internet :: WWW/HTTP',
42 | 'Topic :: Office/Business',
43 | 'Topic :: Software Development :: Libraries :: Python Modules',
44 | ],
45 | keywords='dmarc email spf dkim',
46 | )
47 |
--------------------------------------------------------------------------------