diff --git a/admin/freeposte/__init__.py b/admin/freeposte/__init__.py index 4e284305..c79fd0f7 100644 --- a/admin/freeposte/__init__.py +++ b/admin/freeposte/__init__.py @@ -20,7 +20,9 @@ default_config = { 'HOSTNAME': 'mail.freeposte.io', 'DOMAIN': 'freeposte.io', 'POSTMASTER': 'postmaster', - 'DEBUG': False + 'DEBUG': False, + 'DKIM_PATH': '/dkim/{domain}.{selector}.key', + 'DKIM_SELECTOR': 'dkim' } # Load configuration from the environment if available diff --git a/admin/freeposte/admin/dkim.py b/admin/freeposte/admin/dkim.py new file mode 100644 index 00000000..2a9ceeee --- /dev/null +++ b/admin/freeposte/admin/dkim.py @@ -0,0 +1,21 @@ +""" No crypto operation is done on keys. +They are thus represented as ASCII armored PEM. +""" + +from OpenSSL import crypto + + +def gen_key(key_type=crypto.TYPE_RSA, bits=1024): + """ Generate and return a new RSA key. + """ + key = crypto.PKey() + key.generate_key(key_type, bits) + return crypto.dump_privatekey(crypto.FILETYPE_PEM, key) + + +def strip_key(pem): + """ Return only the b64 part of the ASCII armored PEM. + """ + key = crypto.load_privatekey(crypto.FILETYPE_PEM, pem) + public_pem = crypto.dump_publickey(crypto.FILETYPE_PEM, key) + return public_pem.replace(b"\n", b"").split(b"-----")[2] diff --git a/admin/freeposte/admin/models.py b/admin/freeposte/admin/models.py index 47f36990..732d8538 100644 --- a/admin/freeposte/admin/models.py +++ b/admin/freeposte/admin/models.py @@ -1,10 +1,14 @@ -from freeposte.admin import db +from freeposte.admin import db, dkim +from freeposte import app from sqlalchemy.ext import declarative from passlib import context from datetime import datetime import re +import time +import os +import glob # Many-to-many association table for domain managers @@ -34,6 +38,28 @@ class Domain(Base): max_users = db.Column(db.Integer, nullable=False, default=0) max_aliases = db.Column(db.Integer, nullable=False, default=0) + @property + def dkim_key(self): + file_path = app.config["DKIM_PATH"].format( + domain=self.name, selector=app.config["DKIM_SELECTOR"]) + if os.path.exists(file_path): + with open(file_path, "rb") as handle: + return handle.read() + + @dkim_key.setter + def dkim_key(self, value): + file_path = app.config["DKIM_PATH"].format( + domain=self.name, selector=app.config["DKIM_SELECTOR"]) + with open(file_path, "wb") as handle: + handle.write(value) + + @property + def dkim_publickey(self): + return dkim.strip_key(self.dkim_key).decode("utf8") + + def generate_dkim_key(self): + self.dkim_key = dkim.gen_key() + def has_email(self, localpart): for email in self.users + self.aliases: if email.localpart == localpart: diff --git a/admin/freeposte/admin/templates/domain/details.html b/admin/freeposte/admin/templates/domain/details.html index f78b4244..05488b51 100644 --- a/admin/freeposte/admin/templates/domain/details.html +++ b/admin/freeposte/admin/templates/domain/details.html @@ -10,7 +10,7 @@ Domain details {% block main_action %} {% if current_user.global_admin %} -Regenerate keys +Regenerate keys {% endif %} {% endblock %} @@ -26,8 +26,18 @@ Domain details
{{ domain.name }}. 600 IN MX 10 {{ config["HOSTNAME"] }}.
- DNS SPF entry -
{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"
+ DNS SPF entries +
+{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"
+{{ domain.name }}. 600 IN SPF "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"
+ + + DKIM public key +
{{ domain.dkim_publickey }}
+ + + DNS DKIM entry +
{{ config["DKIM_SELECTOR"] }}._domainkey IN 600 TXT "v=DKIM1; k=rsa; p={{ domain.dkim_publickey }}"
DNS DMARC entry diff --git a/admin/freeposte/admin/views/domains.py b/admin/freeposte/admin/views/domains.py index 4584b96b..dd242816 100644 --- a/admin/freeposte/admin/views/domains.py +++ b/admin/freeposte/admin/views/domains.py @@ -63,3 +63,11 @@ def domain_details(domain_name): domain = utils.get_domain_admin(domain_name) return flask.render_template('domain/details.html', domain=domain, config=flask_app.config) + + +@app.route('/domain/genkeys/', methods=['GET']) +def domain_genkeys(domain_name): + domain = utils.get_domain_admin(domain_name) + domain.generate_dkim_key() + return flask.redirect( + flask.url_for(".domain_details", domain_name=domain_name)) diff --git a/admin/requirements.txt b/admin/requirements.txt index 218c5887..acd52c92 100644 --- a/admin/requirements.txt +++ b/admin/requirements.txt @@ -6,6 +6,7 @@ Flask-migrate Flask-script flask_wtf WTForms-Components +PyOpenSSL passlib gunicorn docker-py diff --git a/docker-compose.yml b/docker-compose.yml index befc010f..9a3a9cb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,7 @@ services: env_file: freeposte.env volumes: - /freeposte/filter:/data + - /freeposte/dkim:/dkim antispam: build: rspamd @@ -79,6 +80,7 @@ services: env_file: freeposte.env volumes: - /freeposte/freeposte:/data + - /freeposte/dkim:/dkim - /var/run/docker.sock:/var/run/docker.sock:ro webmail: