├── common ├── __init__.py ├── mail_receiver_utils.py ├── config.py └── wait-for-it.sh ├── app ├── src │ ├── __init__.py │ ├── logging.py │ ├── tags.py │ ├── templates.py │ ├── resolver.py │ ├── db.py │ ├── check_results.py │ ├── worker.py │ ├── app_utils.py │ ├── app.py │ └── template_utils.py ├── translations │ ├── requirements.txt │ ├── messages.pot │ ├── en_US │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── lt_LT │ │ └── LC_MESSAGES │ │ │ └── messages.po │ └── pl_PL │ │ └── LC_MESSAGES │ │ └── messages.po ├── static │ ├── fonts │ │ ├── bootstrap-icons.woff │ │ └── bootstrap-icons.woff2 │ ├── index.css │ └── images │ │ └── spinner.svg ├── templates │ ├── custom_failed_check_results_hints.html │ ├── custom_layout.html │ ├── 404.html │ ├── check_running.html │ ├── custom_root_layout.html │ ├── check_domain.html │ ├── base.html │ ├── root.html │ ├── check_email.html │ └── check_results.html ├── requirements.txt └── docker │ └── Dockerfile ├── scan ├── libmailgoose │ ├── __init__.py │ ├── languages.txt │ ├── language.py │ ├── logging.py │ └── lax_record_query.py ├── requirements.txt └── setup.py ├── docs ├── .gitignore ├── user-guide │ ├── .gitignore │ ├── configuration.rst │ └── translation.rst ├── requirements.txt ├── index.rst ├── Makefile ├── features.rst ├── generate_config_docs.py ├── conf.py └── quick-start.rst ├── test ├── requirements.txt ├── config.py ├── Dockerfile ├── test_common.py ├── base.py ├── test_api.py ├── test_spf.py └── test_dmarc.py ├── .gitignore ├── env.example ├── .github ├── images │ ├── logo.png │ └── logo_dark.png ├── screenshots │ └── check_results.png ├── workflows │ ├── test.yml │ ├── lint.yml │ ├── check_no_translations_to_update.yml │ └── liccheck.yml └── dependabot.yml ├── mail_receiver ├── requirements.txt ├── Dockerfile └── server.py ├── babel.cfg ├── docker-bind9 ├── Dockerfile └── named.conf ├── setup.cfg ├── .readthedocs.yml ├── scripts ├── test └── update_translation_files ├── .mypy.ini ├── pyproject.toml ├── .pre-commit-config.yaml ├── docker-compose.test.yml ├── LICENSE ├── docker-compose.yml └── README.md /common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scan/libmailgoose/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | venv/ 3 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | logs/ 3 | .env 4 | -------------------------------------------------------------------------------- /docs/user-guide/.gitignore: -------------------------------------------------------------------------------- 1 | config-docs.inc 2 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | APP_DOMAIN=change-this.example.com 2 | -------------------------------------------------------------------------------- /scan/libmailgoose/languages.txt: -------------------------------------------------------------------------------- 1 | pl_PL 2 | en_US 3 | lt_LT 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | python-decouple==3.8 2 | Sphinx==9.0.4 3 | sphinx-rtd-theme==3.0.2 4 | -------------------------------------------------------------------------------- /test/config.py: -------------------------------------------------------------------------------- 1 | TEST_DOMAIN = "test.mailgoose.cert.pl" 2 | APP_URL = "http://app:8000" 3 | -------------------------------------------------------------------------------- /.github/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERT-Polska/mailgoose/HEAD/.github/images/logo.png -------------------------------------------------------------------------------- /mail_receiver/requirements.txt: -------------------------------------------------------------------------------- 1 | aiosmtpd==1.4.6 2 | python-decouple==3.8 3 | redis==5.0.1 4 | /scan 5 | -------------------------------------------------------------------------------- /.github/images/logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERT-Polska/mailgoose/HEAD/.github/images/logo_dark.png -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [jinja2: **/**.html] 2 | encoding = utf-8 3 | extensions = src.tags.BuildIDTag,src.tags.LanguageTag 4 | -------------------------------------------------------------------------------- /app/translations/requirements.txt: -------------------------------------------------------------------------------- 1 | Babel==2.17.0 2 | Jinja2==3.1.6 3 | jinja2-simple-tags==0.6.1 4 | python-decouple==3.8 5 | -------------------------------------------------------------------------------- /.github/screenshots/check_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERT-Polska/mailgoose/HEAD/.github/screenshots/check_results.png -------------------------------------------------------------------------------- /app/static/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERT-Polska/mailgoose/HEAD/app/static/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /app/static/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERT-Polska/mailgoose/HEAD/app/static/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /docker-bind9/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM internetsystemsconsortium/bind9:9.18 2 | 3 | ENV TZ=UTC 4 | 5 | COPY docker-bind9/named.conf /etc/bind/named.conf 6 | -------------------------------------------------------------------------------- /app/templates/custom_failed_check_results_hints.html: -------------------------------------------------------------------------------- 1 | {# If you have any additional text to display if the checks have failed, please put it here. #} 2 | -------------------------------------------------------------------------------- /scan/requirements.txt: -------------------------------------------------------------------------------- 1 | checkdmarc==5.10.12 2 | dkimpy==1.1.8 3 | PyNaCl==1.6.1 4 | pyspf==2.0.14 5 | python-multipart==0.0.21 6 | validators==0.35.0 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # whitespace before :, too long line, line break before binary operator 3 | ignore = E203,E501,W503 4 | exclude = .venv.translations,docs/venv 5 | -------------------------------------------------------------------------------- /scan/libmailgoose/language.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pathlib import Path 3 | 4 | with open(Path(__file__).parent / "languages.txt") as f: 5 | Language = Enum("Language", {line.strip(): line.strip() for line in f}) # type: ignore 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.12" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /app/templates/custom_layout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {# If you want to change the layout in order to provide e.g. custom additional scripts #} 4 | {# or a navbar with logo, mount a different file to this path using Docker volume mount. #} 5 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker compose -f docker-compose.test.yml down --remove-orphans 4 | docker compose -f docker-compose.test.yml up -d --build 5 | 6 | docker compose -f docker-compose.test.yml build test 7 | docker compose -f docker-compose.test.yml run test 8 | -------------------------------------------------------------------------------- /docs/user-guide/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration options 2 | ===================== 3 | 4 | Mailgoose can be configured by setting the following variables in the ``.env`` file (in the form of ``VARIABLE_NAME=VARIABLE_VALUE`` 5 | directives, e.g. ``APP_DOMAIN=example.com``): 6 | 7 | .. include:: config-docs.inc 8 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | COPY test/requirements.txt /requirements.txt 4 | RUN pip install -r /requirements.txt 5 | 6 | COPY common/wait-for-it.sh /wait-for-it.sh 7 | 8 | RUN mkdir /test 9 | COPY test/*.py /test 10 | 11 | WORKDIR /test/ 12 | 13 | CMD python -m unittest discover 14 | -------------------------------------------------------------------------------- /app/src/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def build_logger(name: str) -> logging.Logger: 5 | logger = logging.getLogger(name) 6 | logger.setLevel(logging.INFO) 7 | handler = logging.StreamHandler() 8 | handler.setLevel(logging.INFO) 9 | logger.addHandler(handler) 10 | return logger 11 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | /scan/ 2 | dacite==1.8.1 3 | email-validator==2.1.0.post1 4 | fastapi==0.109.1 5 | Jinja2==3.1.6 6 | jinja2-simple-tags==0.6.1 7 | psycopg2-binary==2.9.9 8 | python-decouple==3.8 9 | redis==5.0.1 10 | rq==1.15.1 11 | SQLAlchemy==2.0.25 12 | uvicorn==0.26.0 13 | -r translations/requirements.txt 14 | -------------------------------------------------------------------------------- /common/mail_receiver_utils.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | 4 | def get_key_from_username(email_username: str) -> bytes: 5 | prefix = "message-" 6 | allowed_characters = string.ascii_letters + string.digits + ".-" 7 | return (prefix + "".join(char for char in email_username if char in allowed_characters)).encode("ascii") 8 | -------------------------------------------------------------------------------- /scan/libmailgoose/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | def build_logger(name: str) -> logging.Logger: 5 | logger = logging.getLogger(name) 6 | logger.setLevel(logging.INFO) 7 | handler = logging.StreamHandler() 8 | handler.setLevel(logging.INFO) 9 | logger.addHandler(handler) 10 | return logger 11 | -------------------------------------------------------------------------------- /docker-bind9/named.conf: -------------------------------------------------------------------------------- 1 | acl anyone { 2 | 0.0.0.0/0; 3 | }; 4 | 5 | options { 6 | directory "/var/cache/bind"; 7 | 8 | dnssec-validation auto; 9 | 10 | recursion yes; 11 | 12 | max-cache-ttl 1; 13 | max-ncache-ttl 1; 14 | 15 | allow-recursion { anyone; }; 16 | 17 | listen-on-v6 { any; }; 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "tests" 2 | on: 3 | pull_request: 4 | branches: [ '**' ] 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 20 10 | steps: 11 | - name: Check out repository 12 | uses: actions/checkout@v2 13 | - name: run tests 14 | run: ./scripts/test 15 | -------------------------------------------------------------------------------- /mail_receiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine3.18 2 | 3 | RUN apk add tzdata git 4 | 5 | ENV TZ=Europe/Warsaw 6 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 7 | 8 | COPY scan /scan 9 | COPY mail_receiver/requirements.txt /requirements.txt 10 | RUN pip install -r /requirements.txt 11 | 12 | COPY common /opt/common/ 13 | COPY mail_receiver/server.py /opt/server.py 14 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to mailgoose documentation! 2 | ==================================== 3 | 4 | Mailgoose is a web application that allows the users to check whether their SPF, DMARC and 5 | DKIM configuration is set up correctly. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Contents: 10 | 11 | quick-start 12 | features 13 | user-guide/configuration 14 | user-guide/translation 15 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy-checkdmarc.*] 2 | ignore_missing_imports = True 3 | 4 | [mypy-decouple.*] 5 | ignore_missing_imports = True 6 | 7 | [mypy-dkim.*] 8 | ignore_missing_imports = True 9 | 10 | [mypy-libmailgoose.*] 11 | ignore_missing_imports = True 12 | 13 | [mypy-sphinx_rtd_theme.*] 14 | ignore_missing_imports = True 15 | 16 | [mypy-jinja2_simple_tags.*] 17 | ignore_missing_imports = True 18 | 19 | [mypy-publicsuffixlist.*] 20 | ignore_missing_imports = True 21 | 22 | [mypy-validators.*] 23 | ignore_missing_imports = True 24 | 25 | [mypy-spf.*] 26 | ignore_missing_imports = True 27 | -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "custom_layout.html" %} 2 | 3 | {% block body %} 4 |
9 | {% trans trimmed %} 10 | Verify your DKIM, DMARC, and SPF settings by sending a test e-mail. 11 | {% endtrans %} 12 |
13 |14 | 15 | {% trans trimmed %} 16 | This is the recommended path. Sending a test e-mail allows us to perform more accurate analysis. 17 | {% endtrans %} 18 | 19 |
20 |32 | {% trans trimmed %} 33 | Verify your SPF and DMARC settings by providing a domain. Note: only the SPF 34 | and DMARC mechanisms will be checked. To check DKIM, you need to send a test e-mail. 35 | {% endtrans %} 36 |
37 |
5 | 60 | {% trans %}As soon as the message is received, the page will automatically refresh - you will then see the check results.{% endtrans %} 61 |
62 |63 | {% trans %}If after a while you still don't see the results, that means that we didn't receive your message. In that case:{% endtrans %} 64 |
65 |177 | {% trans trimmed %} 178 | Because the DMARC record is configured correctly, the SPF record is not required. Sending e-mail 179 | messages from this domain without using the SPF mechanism is still possible - in that case, the messages 180 | need to have correct DKIM signatures. 181 | {% endtrans %} 182 |
183 |184 | {% trans trimmed %} 185 | However, we recommend configuring an SPF record if possible (even if the domain is not used 186 | to send e-mails), because older mail servers may not support DMARC and use SPF for verification. 187 | The combination of all protection mechanisms - SPF, DKIM and DMARC allows all servers to properly 188 | verify e-mail message authenticity. 189 | {% endtrans %} 190 |
191 || {% trans %}Domain{% endtrans %} | 200 |{{ envelope_domain }} |
201 |
| {% trans %}Record{% endtrans %} | 206 |{{ result.domain.spf.record }} |
207 |
| {% if result.domain.spf.record_candidates|length > 1 %}{% trans %}Records{% endtrans %}{% else %}{% trans %}Record{% endtrans %}{% endif %} | 211 |
212 | {% for record in result.domain.spf.record_candidates %}
213 | {{ record }}
214 | {% endfor %}
215 | |
216 |
| {% trans %}Warnings{% endtrans %} | 220 |{% if result.domain.spf.warnings %}{{ render_problems(result.domain.spf.warnings) }}{% else %}{% trans %}none{% endtrans %}{% endif %} | 221 |
| {% trans %}Errors{% endtrans %} | 224 |{% if result.domain.spf.errors %}{{ render_problems(result.domain.spf.errors) }}{% else %}{% trans %}none{% endtrans %}{% endif %} | 225 |
| {% trans %}Domain{% endtrans %} | 240 |{{ from_domain }} |
241 |
| {% trans %}Record{% endtrans %} | 246 |{{ result.domain.dmarc.record }} |
247 |
| {% if result.domain.dmarc.record_candidates|length > 1 %}{% trans %}Records{% endtrans %}{% else %}{% trans %}Record{% endtrans %}{% endif %} | 251 |
252 | {% for record in result.domain.dmarc.record_candidates %}
253 | {{ record }}
254 | {% endfor %}
255 | |
256 |
| {% trans %}Warnings{% endtrans %} | 260 |{% if result.domain.dmarc.warnings %}{{ render_problems(result.domain.dmarc.warnings) }}{% else %}{% trans %}none{% endtrans %}{% endif %} | 261 |
| {% trans %}Errors{% endtrans %} | 264 |{% if result.domain.dmarc.errors %}{{ render_problems(result.domain.dmarc.errors) }}{% else %}{% trans %}none{% endtrans %}{% endif %} | 265 |
| {% trans %}Domain{% endtrans %} | 281 |{{ dkim_domain }} |
282 |
| {% trans %}Warnings{% endtrans %} | 286 |{% if result.dkim.warnings %}{{ render_problems(result.dkim.warnings) }}{% else %}{% trans %}none{% endtrans %}{% endif %} | 287 |
| {% trans %}Errors{% endtrans %} | 290 |{% if result.dkim.errors %}{{ render_problems(result.dkim.errors) }}{% else %}{% trans %}none{% endtrans %}{% endif %} | 291 |
302 | {% trans trimmed %} 303 | To increase the chance that your configuration is interpreted by all e-mail servers correctly, 304 | we recommend fixing all errors and warnings. 305 | {% endtrans %} 306 |
307 | {% endif %} 308 | 309 |310 | {% trans %}After fixing the issues, please rerun the scan - some problems can be detected only if earlier checks complete successfully.{% endtrans %} 311 |
312 | {% include "custom_failed_check_results_hints.html" %} 313 |