diff --git a/README.md b/README.md index bc59b0b..c6b3995 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ smtp_host = "localhost" # required if email.enabled # smtp_port = 25 # default: 25 # smtp_starttls = false # default: false from_address = "ddns@example.com" # required if email.enabled +# change_notification_template = "/etc/ddns-service/change_notification.j2" # optional +# expiry_notification_template = "/etc/ddns-service/expiry_notification.j2" # optional [rate_limit] # enabled = true # default: true diff --git a/files/change_notification.j2 b/files/change_notification.j2 new file mode 100644 index 0000000..b00b3a8 --- /dev/null +++ b/files/change_notification.j2 @@ -0,0 +1,9 @@ +Your dynamic DNS entry has changed. + +Hostname: {{fqdn}} +{% if ipv4_changed %} +IPv4 address: {{ipv4}} (changed at: {{last_ipv4_update}}) +{% endif %} +{% if ipv6_changed %} +IPv6 address: {{ipv6}} (changed at: {{last_ipv6_update}}) +{% endif %} diff --git a/files/config.example.toml b/files/config.example.toml index 04cdd2e..92840f5 100644 --- a/files/config.example.toml +++ b/files/config.example.toml @@ -30,6 +30,8 @@ path = "/var/lib/ddns-service/ddns.db" # required for sqlite # dns_timeout = 5 # default, seconds # ddns_default_key_file = "/etc/ddns-service/ddns.key" # optional, BIND TSIG key # cleanup_interval = 60 # default, seconds +# change_notification_template = "/etc/ddns-service/change_notification.j2" # optional +# expiry_notification_template = "/etc/ddns-service/expiry_notification.j2" # optional # Per-zone TSIG key overrides (optional) # [dns_service.zone_keys] diff --git a/files/ddns-service-9999.ebuild b/files/ddns-service-9999.ebuild index 8f184d1..b1965be 100644 --- a/files/ddns-service-9999.ebuild +++ b/files/ddns-service-9999.ebuild @@ -55,6 +55,7 @@ python_install_all() { dodir /etc/${PN} insinto /etc/${PN} + doins files/*.j2 insopts -m640 newins files/config.example.toml config.toml fowners -R ddns:ddns /etc/${PN} diff --git a/files/expiry_notification.j2 b/files/expiry_notification.j2 new file mode 100644 index 0000000..ec83fb5 --- /dev/null +++ b/files/expiry_notification.j2 @@ -0,0 +1,12 @@ +Your dynamic DNS entry has expired due to inactivity. + +Hostname: {{fqdn}} +{% if ipv4_expired %} +IPv4 address: {{last_ipv4}} (last update: {{last_ipv4_update}}) +{% endif %} +{% if ipv6_expired %} +IPv6 address: {{last_ipv6}} (last update: {{last_ipv6_update}}) +{% endif %} +Expiry TTL: {{expiry_ttl}} seconds + +The DNS records have been removed. Update your client to restore them. diff --git a/src/ddns_service/cleanup.py b/src/ddns_service/cleanup.py index 724dea7..936692d 100644 --- a/src/ddns_service/cleanup.py +++ b/src/ddns_service/cleanup.py @@ -57,23 +57,11 @@ def cleanup_expired(app): app.dns_service.delete_record(hostname.hostname, hostname.zone, "AAAA") if app.email_service: - last_ipv4 = last_ipv4_update = None - if ipv4_expired: - last_ipv4 = hostname.last_ipv4 - last_ipv4_update = hostname.last_ipv4_update - last_ipv6 = last_ipv6_update = None - if ipv6_expired: - last_ipv6 = hostname.last_ipv6 - last_ipv6_update = hostname.last_ipv6_update - app.email_service.send_expiry_notification( hostname.user.email, - f"{hostname.hostname}.{hostname.zone}", - last_ipv4, - last_ipv4_update, - last_ipv6, - last_ipv6_update, - hostname.expiry_ttl + hostname, + ipv4_expired, + ipv6_expired ) # Clear IP addresses diff --git a/src/ddns_service/email.py b/src/ddns_service/email.py index 6bc175b..58bbc92 100644 --- a/src/ddns_service/email.py +++ b/src/ddns_service/email.py @@ -5,6 +5,28 @@ import smtplib from . import datetime_str from email.mime.text import MIMEText +from jinja2 import Environment, BaseLoader + + +def load_template(path): + """ + Load template from file. + + Args: + path: Path to template. + Returns: + Template if path was not None, None otherwise. + """ + if path is None: + return + + with open(path) as fh: + data = fh.read() + + return Environment( + loader=BaseLoader(), + trim_blocks=True + ).from_string(data) class EmailService: @@ -20,6 +42,13 @@ class EmailService: self.config = config.get("email", {}) self.enabled = self.config.get("enabled", False) + self.change_template = load_template(self.config.get("change_notification_template", None)) + self.expiry_template = load_template(self.config.get("expiry_notification_template", None)) + + if not self.change_template and not self.expiry_template and self.enabled: + logging.warning("No templates configured, disabling Email") + self.enabled = False + def send(self, to, subject, body): """ Send email using configured SMTP server. @@ -65,15 +94,58 @@ class EmailService: logging.error(f"Email send failed: to={to} error={e}") return False + def send_changed_notification( + self, + email, + hostname, + ipv4_changed, + ipv6_changed + ): + """ + Send hostname changed notification email. + + Args: + email: Recipient email. + hostname: Changed hostname. + ipv4_changed: Wheter IPv4 address changed. + ipv6_changed: Wheter IPv6 address changed. + + Returns: + True if sent successfully. + """ + if not self.enabled: + logging.debug("Email disabled, skipping") + return False + + if not self.change_template: + logging.debug("No change notification template configured, skipping") + return False + + fqdn = f"{hostname.hostname}.{hostname.zone}" + template_variables = { + "hostname": hostname.hostname, + "zone": hostname.zone, + "fqdn": fqdn, + "ipv4_changed": ipv4_changed, + "ipv4": hostname.last_ipv4, + "last_ipv4_update": datetime_str(hostname.last_ipv4_update), + "ipv6_changed": ipv6_changed, + "ipv6": hostname.last_ipv6, + "last_ipv6_update": datetime_str(hostname.last_ipv6_update), + "expiry_ttl": hostname.expiry_ttl, + } + + subject = f"DDNS hostname expired: {fqdn}" + body = self.expiry_template.render(**template_variables) + + return self.send(email, subject, body) + def send_expiry_notification( self, email, hostname, - last_ipv4, - last_ipv4_update, - last_ipv6, - last_ipv6_update, - expiry_ttl + ipv4_expired, + ipv6_expired ): """ Send hostname expiry notification email. @@ -81,27 +153,35 @@ class EmailService: Args: email: Recipient email. hostname: Expired hostname. - last_ipv4: Tuple containing last IPv4 address and last update timestamp. - last_ipv6: Tuple containing last IPv6 address and last update timestamp. - expiry_ttl: Expiry TTL in seconds. + ipv4_expired: Wheter IPv4 address expired. + ipv6_expired: Wheter IPv6 address expired. Returns: True if sent successfully. """ + if not self.enabled: + logging.debug("Email disabled, skipping") + return False - expired = "" - if last_ipv4 and last_ipv4_update: - expired += f"IPv4 address: {last_ipv4} (last update: {last_ipv4_update})\n" - if last_ipv6 and last_ipv6_update: - expired += f"IPv6 address: {last_ipv6} (last update: {last_ipv6_update})\n" + if not self.expiry_template: + logging.debug("No expiry notification template configured, skipping") + return False - subject = f"DDNS hostname expired: {hostname}" - body = f"""Your dynamic DNS entry has expired due to inactivity. + fqdn = f"{hostname.hostname}.{hostname.zone}" + template_variables = { + "hostname": hostname.hostname, + "zone": hostname.zone, + "fqdn": fqdn, + "ipv4_expired": ipv4_expired, + "last_ipv4": hostname.last_ipv4, + "last_ipv4_update": datetime_str(hostname.last_ipv4_update), + "ipv6_expired": ipv6_expired, + "last_ipv6": hostname.last_ipv6, + "last_ipv6_update": datetime_str(hostname.last_ipv6_update), + "expiry_ttl": hostname.expiry_ttl, + } -Hostname: {hostname} -{expired} -Expiry TTL: {expiry_ttl} seconds + subject = f"DDNS hostname expired: {fqdn}" + body = self.expiry_template.render(**template_variables) -The DNS records have been removed. Update your client to restore them. -""" return self.send(email, subject, body)