mailcow / mailcow-dockerized

mailcow: dockerized - 🐮 + 🐋 = 💕
https://mailcow.email
GNU General Public License v3.0
8.7k stars 1.17k forks source link

Aliases for all domains #4507

Open floli opened 2 years ago

floli commented 2 years ago

Summary

Allow to create an alias for all domains, currently existing ones as well as newly created domains.

Motivation

Use case would mainly be for RFC required aliases, such as postmaster (or hostmaster, webmaster, ...) which is usually the same person for all domains.

Creating the aliases for every new domain is tedious and likely to be forgotten, resulting in an unreachable postmaster address.

Additional context

As we can create a catchall-all-users alias using @example.org, a complementary syntax would be to use postmaster@ as a catch-all-domains alias.

floli commented 2 years ago

I hacked together a little python script that creates aliases for all domains:

from urllib.request import Request, urlopen
import json, re, itertools
import logging

class MailcowAPI:
    def __init__(self, domain: str, key: str):
        self.domain = domain
        self.key = key

    def rest_call(self, path: str, body = None):
        url = "https://" + self.domain + "/api/v1/" + path
        headers = {"accept" : "application/json",
                   "X-API-Key" : self.key,
                   'content-type' : 'application/json'}
        data = json.dumps(body).encode("utf-8") if body else None
        request = Request(url, data, headers)
        logging.debug("Opening URL: %s with body: %s", url, body)
        with urlopen(request) as response:
            content = response.read().decode("utf-8")

            try:
                json_content = json.loads(content)
                message_type = json_content["type"]
                message_content = json_content["msg"]
            except (json.JSONDecodeError, AttributeError, TypeError):
                pass
            else:
                if message_type == "error":
                    raise Exception(f"Error calling URL {url} with data {data} returned with message: {message_content}.")

        return json.loads(content)

    def mailboxes(self):
        return self.rest_call(path = "get/mailbox/all")

    def logs(self, log_type: str, count: int = 10):
        return self.rest_call("get/logs/" + log_type + "/" + str(count))

    def aliases(self, id = "all"):
        return self.rest_call("get/alias/" + str(id))

    def add_alias(self, address, goto):
        self.rest_call("add/alias", {"address" : address, "goto" : goto, "active" : True})
        logging.debug("Created alias: %s -> %s", address, goto)

    def delete_alias(self, id: int):
        self.rest_call("delete/alias", body = [str(id)])
        logging.debug("Deleted alias %s", id)

    def domains(self, id = "all"):
        return self.rest_call("get/domain/" + str(id))

def set_aliases_for_every_domain(mc: MailcowAPI):
    target = "me@example.invalid"
    aliases = ["postmaster", "hostmaster"]
    domain_regexp = r".*"
    force = False  # delete existing aliases and recreate

    domains = [ i["domain_name"] for i in mc.domains() if re.match(domain_regexp, i["domain_name"]) ]
    existing_aliases = { i["address"]: i["id"] for i in mc.aliases() }
    aliases_to_create = [ a+"@"+d for a in aliases for d in domains ]

    for a in aliases_to_create:
        if a in existing_aliases and force is False:
            logging.info("Aliases for %s already exists, skipping.", a)
        elif a in existing_aliases and force is True:
            logging.info("Aliases for %s already exists, deleting and set again.", a)
            mc.delete_alias(existing_aliases[a])
            mc.add_alias(a, target)
        else:
            mc.add_alias(a, target)

def main():
    logging.basicConfig(level = logging.DEBUG)
    key = "An API key with write permissions"
    domain = "the.mail.domain"

    mc = MailcowAPI(domain, key)
    set_aliases_for_every_domain(mc)

main()

Fell free to use, modify and distribute (public domain).