diff --git a/.gitignore b/.gitignore index e793e74..21a316a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/.env **/__pycache__ **/*.pem +**/.DS_Store diff --git a/dns-config/README.md b/dns-config/README.md new file mode 100644 index 0000000..eaeda77 --- /dev/null +++ b/dns-config/README.md @@ -0,0 +1,29 @@ +# Mail-in-a-Box DNS Configuration Module + +The `dns-config` module provides a command-line interface and a module to configure Mail-in-a-Box's server to delegate a DNS zone to Azure DNS. It retrieves the nameservers for a given Azure DNS zone and sets up the necessary DNS records in Mail-in-a-Box to delegate the zone to Azure DNS. + +The module also contains additional commands to list Azure or Mail-in-a-Box DNS records. It can also be used to set, update, add or delete DNS records in Mail-in-a-Box. + +## Usage + +The following commands are available in the `dns-config` module: + +- `list-azure`: List Azure DNS records for a given resource group and zone name. +- `list-miab`: List Mail-in-a-Box DNS records of a given type (A, CNAME, MX, etc.). +- `set`: Set a DNS record in Mail-in-a-Box. +- `add`: Add a DNS record in Mail-in-a-Box. +- `delete`: Delete a DNS record in Mail-in-a-Box. + +Each command has its own `--help` option that provides more information on how to use it. + +To automatically configure an Azure DNS zone in Mail-in-a-Box, you can use the following command: + +```bash +python -m dns-config configure-miab --resource-group --zone-name +``` + +Then list the Mail-in-a-Box DNS records to verify that the delegation records have been added: + +```bash +python -m dns-config list-miab --type NS +``` diff --git a/dns-config/__init__.py b/dns-config/__init__.py new file mode 100644 index 0000000..b785ceb --- /dev/null +++ b/dns-config/__init__.py @@ -0,0 +1 @@ +# Module diff --git a/dns-config/__main__.py b/dns-config/__main__.py new file mode 100644 index 0000000..7929f3d --- /dev/null +++ b/dns-config/__main__.py @@ -0,0 +1,92 @@ +import argparse +from .azure import get_dns_nameservers +from . import miab + +def azure_list_command(args): + nameservers = get_dns_nameservers(args.resource_group, args.zone_name) + print(f"Nameservers for zone {args.zone_name}:") + for ns in nameservers: + print(f" - {ns}") + +# Mail-in-a-Box API wrapper functions +def miab_set_command(args): + return miab.set_custom_dns(args.domain_name, args.record_type, args.record_value) + +def miab_delete_command(args): + return miab.delete_custom_dns(args.domain_name, args.record_type) + +def miab_add_command(args): + return miab.add_custom_dns(args.domain_name, args.record_type, args.record_value) + +def miab_list_command(args): + records = miab.list_custom_dns(args.record_type) + if args.record_type: + print(f"Custom {args.record_type} records:") + else: + print("Custom DNS records:") + + for record in records: + print(f" - {record['name']} ({record['type']}): {record['value']}") + +def miab_configure_command(args): + # Get a list of nameservers for the specified Azure DNS zone + nameservers = get_dns_nameservers(args.resource_group, args.zone_name) + + if not nameservers: + print("No nameservers found. Aborting configuration.") + return + + # Add each nameserver as an NS record in the Mail-in-a-Box DNS server + print(f"Configuring Mail-in-a-Box with nameservers from Azure DNS zone {args.zone_name}...") + for ns in nameservers: + print(f"Adding NS record for {ns}...") + success = miab.add_custom_dns(args.zone_name, "NS", ns) + if success: + print(f"Successfully added NS record for {ns}") + else: + print(f"Failed to add NS record for {ns}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="DNS Configuration Tool") + subparsers = parser.add_subparsers() + + # list command - lists nameservers for a given Azure DNS zone + list_azure_parser = subparsers.add_parser("list-azure", help="List nameservers for an Azure DNS zone") + list_azure_parser.add_argument("--resource-group", required=True, help="Resource group name") + list_azure_parser.add_argument("--zone-name", required=True, help="DNS zone name") + list_azure_parser.set_defaults(func=azure_list_command) + + # list-miab command - lists custom DNS records in the Mail-in-a-Box DNS server + list_miab_parser = subparsers.add_parser("list-miab", help="List custom DNS records in the Mail-in-a-Box DNS server") + list_miab_parser.add_argument("--record-type", help="Filter by DNS record type (e.g., A, CNAME, MX)") + list_miab_parser.set_defaults(func=miab_list_command) + + # set command - sets a record in the Mail-in-a-Box DNS server + set_parser = subparsers.add_parser("set", help="Set a DNS record in the Mail-in-a-Box DNS server") + set_parser.add_argument("--domain-name", required=True, help="Domain name to set the record for") + set_parser.add_argument("--record-type", required=True, help="DNS record type (e.g., A, CNAME, MX)") + set_parser.add_argument("--record-value", required=True, help="Value for the DNS record") + set_parser.set_defaults(func=miab_set_command) + + # delete command - deletes a record from the Mail-in-a-Box DNS server + delete_parser = subparsers.add_parser("delete", help="Delete a DNS record from the Mail-in-a-Box DNS server") + delete_parser.add_argument("--domain-name", required=True, help="Domain name to delete the record for") + delete_parser.add_argument("--record-type", required=True, help="DNS record type (e.g., A, CNAME, MX)") + delete_parser.add_argument("--record-value", help="Value for the DNS record") + delete_parser.set_defaults(func=miab_delete_command) + + # add command - adds a record to the Mail-in-a-Box DNS server + add_parser = subparsers.add_parser("add", help="Add a DNS record to the Mail-in-a-Box DNS server") + add_parser.add_argument("--domain-name", required=True, help="Domain name to add the record for") + add_parser.add_argument("--record-type", required=True, help="DNS record type (e.g., A, CNAME, MX)") + add_parser.add_argument("--record-value", required=True, help="Value for the DNS record") + add_parser.set_defaults(func=miab_add_command) + + # configure-miab - retrieves nameservers for an Azure DNS zone and configures them in the Mail-in-a-Box DNS server + configure_miab_parser = subparsers.add_parser("configure-miab", help="Configure Mail-in-a-Box DNS server with nameservers from an Azure DNS zone") + configure_miab_parser.add_argument("--resource-group", required=True, help="Azure Resource group name containing the DNS zone") + configure_miab_parser.add_argument("--zone-name", required=True, help="DNS zone name") + configure_miab_parser.set_defaults(func=miab_configure_command) + + args = parser.parse_args() + args.func(args) diff --git a/dns-config/azure.py b/dns-config/azure.py new file mode 100644 index 0000000..9e90e48 --- /dev/null +++ b/dns-config/azure.py @@ -0,0 +1,15 @@ +from azure.identity import DefaultAzureCredential +from azure.mgmt.dns import DnsManagementClient +import os + +AZURE_SUBSCRIPTION_ID = os.environ.get("AZURE_SUBSCRIPTION_ID", None) + +def get_dns_nameservers(resource_group_name: str, zone_name: str, azure_subscription_id: str | None = None) -> list[str]: + try: + creds = DefaultAzureCredential() + dns_client = DnsManagementClient(creds, azure_subscription_id or AZURE_SUBSCRIPTION_ID) + zone = dns_client.zones.get(resource_group_name, zone_name) + return zone.name_servers + except Exception as e: + print(f"Error fetching DNS nameservers: {e}") + return [] diff --git a/dns-config/miab.py b/dns-config/miab.py new file mode 100644 index 0000000..85ffa4a --- /dev/null +++ b/dns-config/miab.py @@ -0,0 +1,67 @@ +from os import getenv +from base64 import b64encode +from urllib import parse +from jmespath import search +import requests + +MIAB_USERNAME = getenv('MIAB_USERNAME', None) or getenv('MAILINABOX_EMAIL', None) +MIAB_PASSWORD = getenv('MIAB_PASSWORD', None) or getenv('MAILINABOX_PASSWORD', None) +MIAB_HOST = getenv('MIAB_HOST', None) + +try: + MIAB_HOST = parse.urlparse(getenv('MAILINABOX_BASE_URL', None)).hostname +except Exception: + pass + +# Prepare the Basic Auth header for MIAB API requests +MIAB_AUTH_HEADER = "Basic " + b64encode(f"{MIAB_USERNAME}:{MIAB_PASSWORD}".encode()).decode() + +def get_miab_url(domain_name: str, record_type: str = "A") -> str: + if record_type == "A": + return f"https://{MIAB_HOST}/admin/dns/custom/{domain_name}" + else: + return f"https://{MIAB_HOST}/admin/dns/custom/{domain_name}/{record_type.upper()}" + +def set_custom_dns(domain_name: str, record_type: str = "A", record_value: str | None = None) -> bool: + if not MIAB_HOST or not MIAB_USERNAME or not MIAB_PASSWORD: + raise Exception("MIAB configuration is incomplete. Please set MIAB_HOST, MIAB_USERNAME, and MIAB_PASSWORD.") + + url = get_miab_url(domain_name, record_type) + payload = record_value if record_value else "" + response = requests.put(url, data=payload, headers={"Authorization": MIAB_AUTH_HEADER, "Content-Type": "text/plain"}) + return response.status_code == 200 + +def delete_custom_dns(domain_name: str, record_type: str = "A", record_value: str | None = None) -> bool: + if not MIAB_HOST or not MIAB_USERNAME or not MIAB_PASSWORD: + raise Exception("MIAB configuration is incomplete. Please set MIAB_HOST, MIAB_USERNAME, and MIAB_PASSWORD.") + + url = get_miab_url(domain_name, record_type) + payload = record_value if record_value else "" + response = requests.delete(url, data=payload, headers={"Authorization": MIAB_AUTH_HEADER, "Content-Type": "text/plain"}) + return response.status_code == 200 + +def add_custom_dns(domain_name: str, record_type: str = "A", record_value: str | None = None) -> bool: + if not MIAB_HOST or not MIAB_USERNAME or not MIAB_PASSWORD: + raise Exception("MIAB configuration is incomplete. Please set MIAB_HOST, MIAB_USERNAME, and MIAB_PASSWORD.") + + url = get_miab_url(domain_name, record_type) + payload = record_value if record_value else "" + response = requests.post(url, data=payload, headers={"Authorization": MIAB_AUTH_HEADER, "Content-Type": "text/plain"}) + return response.status_code == 200 + +def list_custom_dns(record_type: str = None): + response = requests.get( + f"https://{MIAB_HOST}/admin/dns/custom", + headers={"Authorization": MIAB_AUTH_HEADER} + ) + + if response.status_code == 200: + records = response.json() + if record_type: + jmespath_expr = f"[?rtype=='{record_type.upper()}']" + else: + jmespath_expr = "[]" + + return search(jmespath_expr + ".{name: qname, type: rtype, value: value}", records) + else: + raise Exception(f"Failed to retrieve DNS records: {response.status_code} {response.text}")