From 8bb7792ee380c14c076f77d7d57d68fb9c863c61 Mon Sep 17 00:00:00 2001 From: n8fr8 Date: Wed, 1 Jul 2026 13:31:24 -0400 Subject: [PATCH] add logic for dynamic IP lookup for deltachat links and install #31 --- app/routes.py | 36 +++++++++++++++++++++++++++++++++++- install_madmail.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/app/routes.py b/app/routes.py index 196e9de..e2bed77 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,6 @@ import io import re +import socket import subprocess from app import app @@ -26,6 +27,39 @@ from install_madmail import run_madmail_installer CHANGES_REQUIRING_RESTART = ['wifi_password', 'ssid', 'enable_access_point', 'enable_chat', 'enable_delta_chat', 'butterbox_hostname', 'ssh_access_settings', 'root_account_settings', 'root_password'] RASPAP_INSTALLED = os.path.exists("/var/www/html/raspap") +def resolve_butterbox_ip() -> str: + """Best-effort lookup of the address clients should use to reach this box. + + ``BUTTERBOX_DEFAULT_IP`` is only correct while the box serves its own + access point. When it's joined to another network its address differs, so + try to discover the real one and only fall back to the configured default. + """ + hostname = get_setting('butterbox_hostname') or app.config['BUTTERBOX_HOSTNAME'] + + # 1. Resolve the box's own mDNS/.local name. This is the same name the + # DeltaChat link uses for the mail host, so avahi/nss-mdns is present. + try: + ip = socket.gethostbyname(f"{hostname}.local") + if ip and not ip.startswith("127."): + return ip + except OSError: + pass + + # 2. Fall back to the address of the primary outbound interface. The + # connect() picks a route without sending any packets. + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + if ip and not ip.startswith("127."): + return ip + except OSError: + pass + + # 3. Give up and use the configured access-point default. + return app.config['BUTTERBOX_DEFAULT_IP'] + + def gen_username() -> str: words = top_n_list("en", 5000) prefix = random.randint(1000, 9999) @@ -370,7 +404,7 @@ def messaging(): @app.route("/deltachat_credentials", methods=["POST"]) def deltachat_credentials(): - ip = app.config['BUTTERBOX_DEFAULT_IP'] + ip = resolve_butterbox_ip() username = gen_username() password = gen_password() hostname = f"{get_setting('butterbox_hostname')}.local" diff --git a/install_madmail.py b/install_madmail.py index c71ea50..9deb016 100644 --- a/install_madmail.py +++ b/install_madmail.py @@ -1,8 +1,36 @@ import pexpect import json +import socket from app import app from subprocess import run +def resolve_butterbox_ip(hostname): + """Best-effort lookup of the box's current address, falling back to the + configured default. Mirrors app.routes.resolve_butterbox_ip but avoids a + circular import (routes imports this module) and reuses the hostname that + was already loaded from settings.txt.""" + # 1. Resolve the box's own mDNS/.local name. + try: + ip = socket.gethostbyname(f"{hostname}.local") + if ip and not ip.startswith("127."): + return ip + except OSError: + pass + + # 2. Fall back to the address of the primary outbound interface. The + # connect() picks a route without sending any packets. + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + if ip and not ip.startswith("127."): + return ip + except OSError: + pass + + # 3. Give up and use the configured access-point default. + return app.config['BUTTERBOX_DEFAULT_IP'] + def run_madmail_installer(): with open("./settings.txt", "r") as f: settings = json.load(f) @@ -18,7 +46,7 @@ def run_madmail_installer(): child.expect("MX record") child.sendline(f"{butterbox_hostname}.local") child.expect("Public IP address") - child.sendline(app.config['BUTTERBOX_DEFAULT_IP']) + child.sendline(resolve_butterbox_ip(butterbox_hostname)) child.expect("Additional domains") child.sendline("") child.expect("State directory")