├── .gitignore ├── README.md ├── privacycow ├── __init__.py ├── config.ini.example └── privacycow.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # tests and coverage 6 | *.pytest_cache 7 | .coverage 8 | *.egg-info 9 | 10 | # database & logs 11 | *.db 12 | *.sqlite3 13 | *.log 14 | 15 | # venv 16 | env 17 | venv 18 | 19 | # other 20 | .DS_Store 21 | 22 | # sphinx docs 23 | _build 24 | _static 25 | _templates 26 | 27 | # javascript 28 | package-lock.json 29 | .vscode/symbols.json 30 | 31 | # config 32 | config.ini 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # privacycow 2 | 3 | ## Install 4 | 5 | ```console 6 | python3 -m pip install git+https://github.com/schemen/privacycow.git#egg=privacycow 7 | ``` 8 | 9 | ### In "developer mode" (editable): 10 | 11 | 12 | using a virtualenv: 13 | ```console 14 | virtualenv venv -p python3 15 | source venv/bin/activate 16 | python -m pip install -e . 17 | ``` 18 | 19 | or 20 | ```console 21 | python3 -m venv venv 22 | source venv/bin/activate 23 | python -m pip install git+ssh://git@github.com:schemen/privacycow.git#egg=privacycow 24 | ``` 25 | 26 | ```x-sh 27 | git clone https://github.com/schemen/privacycow.git && cd privacycow 28 | virtualenv venv -p python3 29 | source venv/bin/activate 30 | pip install -r requirements.txt 31 | deactivate 32 | 33 | ``` 34 | 35 | ### First run, Adjust settings, see options further down 36 | 37 | ``` 38 | # configure your config.ini 39 | # run privacycow once, it will create a sample config file at 40 | # ~/.config/privacycow/config.ini 41 | privacycow && vim ~/.config/privacycow/config.ini 42 | 43 | ``` 44 | ## Config 45 | Have a look at `config.ini.example` for the sample configuration 46 | 47 | ``` 48 | [DEFAULT] 49 | # The default domain used as an alias domain. I recommend purchasing an 50 | # unrelated domain and adding it to your Mailcow installation. Alternatively 51 | # you can just use your main domain. 52 | RELAY_DOMAIN = example.com 53 | # The address emails will go to if a GOTO is not defined in the 54 | # [$RELAY_DOMAIN] settings section. 55 | GOTO = user@example.com 56 | # These two settings should be self explanatory and will be used if 57 | # there is not a MAILCOW_API_KEY or MAILCOW_INSTANCE setting in the 58 | # [$RELAY_DOMAIN] settings section. 59 | MAILCOW_API_KEY = api_key 60 | MAILCOW_INSTANCE = https://mail.example.com 61 | 62 | [example.com] 63 | # The settings to be used when example.com is RELAY_DOMAIN. RELAY_DOMAIN 64 | # in the [DEFAULT] section is defining this section as the default. 65 | # All three parameters are optional here as if they are not defined 66 | # the setting from DEFAULT will be used instead. 67 | GOTO = another@example.com 68 | MAILCOW_API_KEY = another_api_key 69 | MAILCOW_INSTANCE = https://mail.example.com 70 | 71 | [example.org] 72 | # These settings can be used by calling privacycow like this: 73 | # RELAY_DOMAIN=example.org privacycow list 74 | GOTO = user@example.org 75 | # Note we have chosen not to define MAILCOW_API_KEY and 76 | # MAILCOW_INSTANCE here so the values in [DEFAULT] will be used instead. 77 | 78 | 79 | ``` 80 | 81 | ## Usage 82 | ``` 83 | ➜ privacycow 84 | Usage: privacycow [OPTIONS] COMMAND [ARGS]... 85 | 86 | Options: 87 | --debug / --no-debug 88 | --help Show this message and exit. 89 | 90 | Commands: 91 | add Create a new random alias. 92 | delete Delete a alias. 93 | disable Disable a alias, done by setting the "Silently Discard" option. 94 | enable Enable a alias, which disables "Silently Discard". 95 | list Lists all aliases with the configured privacy domain. 96 | 97 | ``` 98 | 99 | 100 | ## Examples 101 | 102 | ``` 103 | ➜ privacycow add -c "terrible place" 104 | Success! The following Alias has been created: 105 | Alias ID: 29 106 | Alias Email: 5b25SxYx9J46lJjk.YVuONdYUUA3ekAgU@privacycow.com 107 | Alias Comment: terrible place 108 | 109 | 110 | ➜ privacycow disable 29 111 | Success! The following Alias disabled: 112 | Alias ID: 29 113 | Alias Email: 5b25SxYx9J46lJjk.YVuONdYUUA3ekAgU@privacyfwd.ch 114 | 115 | 116 | 117 | ➜ privacycow list 118 | ID Alias Comment Active 119 | =============================================================================== 120 | 22 pQf6qmgMxpa8C.c1Q3sdpDZarqf4ggCe2@privacycow.com bad actor Active 121 | 28 mX4WjTjHcJwa96Vk.xEKpPKbgArysoWvg@privacycow.com test Active 122 | 29 5b25SxYx9J46lJjk.YVuONdYUUA3ekAgU@privacycow.com terrible place Discard 123 | 124 | ``` 125 | -------------------------------------------------------------------------------- /privacycow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schemen/privacycow/aa19ffe9f1d4557931a351469ddaaabae6cd4afa/privacycow/__init__.py -------------------------------------------------------------------------------- /privacycow/config.ini.example: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | RELAY_DOMAIN = privacycow.com 3 | GOTO = user@example.com 4 | MAILCOW_API_KEY = api_key 5 | MAILCOW_INSTANCE = https://mail.example.com 6 | 7 | [privacycow.com] 8 | GOTO = alternative@example.com 9 | -------------------------------------------------------------------------------- /privacycow/privacycow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import configparser 3 | import random 4 | import socket 5 | from os import environ as env, makedirs 6 | from os.path import isfile, expanduser 7 | from shutil import copyfile 8 | 9 | import click 10 | import requests 11 | import texttable 12 | import urllib3.util.connection as urllib3_cn 13 | from pkg_resources import Requirement, resource_filename 14 | 15 | 16 | def read_config(file): 17 | config = configparser.ConfigParser() 18 | config.read(file) 19 | return config 20 | 21 | 22 | config_path = expanduser('~/.config/privacycow/') 23 | 24 | if isfile(config_path + "config.ini"): 25 | config = read_config(config_path + "config.ini") 26 | else: 27 | makedirs(config_path, exist_ok=True) 28 | samplefile = resource_filename(Requirement.parse("privacycow"), "privacycow/config.ini.example") 29 | copyfile(samplefile, config_path + "config.ini") 30 | config = read_config(config_path + "config.ini") 31 | click.echo("Privacycow ran for the first time.\nMake sure you check your config file at %s" % config_path + "config.ini") 32 | 33 | RELAY_DOMAIN = env.get('RELAY_DOMAIN', config['DEFAULT']['RELAY_DOMAIN']) 34 | MAILCOW_API_KEY = env.get('MAILCOW_API_KEY') 35 | if not MAILCOW_API_KEY: 36 | if RELAY_DOMAIN in config and 'MAILCOW_API_KEY' in config[RELAY_DOMAIN]: 37 | MAILCOW_API_KEY = config[RELAY_DOMAIN]['MAILCOW_API_KEY'] 38 | else: 39 | MAILCOW_API_KEY = config['DEFAULT']['MAILCOW_API_KEY'] 40 | MAILCOW_INSTANCE = env.get("MAILCOW_INSTANCE") 41 | if not MAILCOW_INSTANCE: 42 | if RELAY_DOMAIN in config and 'MAILCOW_INSTANCE' in config[RELAY_DOMAIN]: 43 | MAILCOW_INSTANCE = config[RELAY_DOMAIN]['MAILCOW_INSTANCE'] 44 | else: 45 | MAILCOW_INSTANCE = config['DEFAULT']['MAILCOW_INSTANCE'] 46 | GOTO = env.get('GOTO') 47 | if not GOTO: 48 | if RELAY_DOMAIN in config and 'GOTO' in config[RELAY_DOMAIN]: 49 | GOTO = config[RELAY_DOMAIN]['GOTO'] 50 | else: 51 | GOTO = config['DEFAULT']['GOTO'] 52 | 53 | 54 | VOWELS = "aeiou" 55 | CONSONANTS = "bcdfghjklmnpqrstvwxyz" 56 | 57 | 58 | @click.group() 59 | @click.option('--debug/--no-debug') 60 | @click.pass_context 61 | def cli(ctx, debug, ): 62 | ctx.ensure_object(dict) 63 | ctx.obj['DEBUG'] = debug 64 | if debug: 65 | click.echo("Debug is enabled") 66 | 67 | 68 | @cli.command() 69 | @click.pass_context 70 | def list(ctx): 71 | """Lists all aliases with the configured privacy domain.""" 72 | API_ENDPOINT = "/api/v1/get/alias/all" 73 | headers = {'X-API-Key': MAILCOW_API_KEY} 74 | 75 | try: 76 | r = requests.get(MAILCOW_INSTANCE + API_ENDPOINT, headers=headers, ) 77 | r.raise_for_status() 78 | except requests.exceptions.HTTPError as err: 79 | raise SystemExit(err) 80 | 81 | table = texttable.Texttable() 82 | table.set_deco(texttable.Texttable.HEADER) 83 | table.set_max_width(0) 84 | table.header(["ID", "Alias", "Comment", "Active"]) 85 | 86 | for i in r.json(): 87 | if i["domain"] == RELAY_DOMAIN: 88 | if i["goto"] == "null@localhost": 89 | active = "Discard" 90 | else: 91 | active = "Active" 92 | 93 | table.add_row([i["id"], i["address"], i["public_comment"], active]) 94 | 95 | click.echo(table.draw()) 96 | 97 | 98 | @cli.command() 99 | @click.option('-g', '--goto', default=GOTO, 100 | help='Goto address "mail@example.com". If no option is passed, GOTO env variable or config.ini will be used.') 101 | @click.option('-c', '--comment', default=None, 102 | help='Public Comment string, use "service description" as an example. If no option is passed, comment will be empty.') 103 | @click.pass_context 104 | def add(ctx, goto, comment): 105 | """Create a new random alias.""" 106 | API_ENDPOINT = "/api/v1/add/alias" 107 | headers = {'X-API-Key': MAILCOW_API_KEY} 108 | 109 | data = {"address": readable_random_string(random.randint(3, 9)) + "." 110 | + readable_random_string(random.randint(3, 9)) + "@" + RELAY_DOMAIN, 111 | "goto": goto, 112 | "public_comment": comment, 113 | "active": 1} 114 | 115 | try: 116 | r = requests.post(MAILCOW_INSTANCE + API_ENDPOINT, headers=headers, json=data) 117 | r.raise_for_status() 118 | except requests.exceptions.HTTPError as err: 119 | raise SystemExit(err) 120 | 121 | data = r.json() 122 | 123 | click.echo("Success! The following Alias has been created:") 124 | click.echo("Alias ID: %s" % data[0]["msg"][2]) 125 | click.echo("Alias Email: %s" % data[0]["msg"][1]) 126 | click.echo("Alias Comment: %s" % data[0]["log"][3]["public_comment"]) 127 | 128 | 129 | @cli.command() 130 | @click.argument('alias_id') 131 | @click.pass_context 132 | def disable(ctx, alias_id): 133 | """Disable a alias, done by setting the "Silently Discard" option. """ 134 | API_ENDPOINT = "/api/v1/edit/alias" 135 | headers = {'X-API-Key': MAILCOW_API_KEY} 136 | 137 | data = {"items": [alias_id], "attr": {"goto_null": "1"}} 138 | 139 | try: 140 | r = requests.post(MAILCOW_INSTANCE + API_ENDPOINT, headers=headers, json=data) 141 | r.raise_for_status() 142 | except requests.exceptions.HTTPError as err: 143 | raise SystemExit(err) 144 | 145 | data = r.json() 146 | 147 | click.echo("Success! The following Alias disabled:") 148 | click.echo("Alias ID: %s" % data[0]["log"][3]["id"][0]) 149 | click.echo("Alias Email: %s" % data[0]["msg"][1]) 150 | 151 | 152 | @cli.command() 153 | @click.argument('alias_id') 154 | @click.option('-g', '--goto', default=GOTO, 155 | help='Goto address "mail@example.com". If no option is passed, GOTO env variable or config.ini will be used.') 156 | @click.pass_context 157 | def enable(ctx, alias_id, goto): 158 | """Enable a alias, which disables "Silently Discard". """ 159 | API_ENDPOINT = "/api/v1/edit/alias" 160 | headers = {'X-API-Key': MAILCOW_API_KEY} 161 | 162 | data = {"items": [alias_id], "attr": {"goto": goto}} 163 | 164 | try: 165 | r = requests.post(MAILCOW_INSTANCE + API_ENDPOINT, headers=headers, json=data) 166 | r.raise_for_status() 167 | except requests.exceptions.HTTPError as err: 168 | raise SystemExit(err) 169 | 170 | data = r.json() 171 | 172 | click.echo("Success! The following Alias enabled:") 173 | click.echo("Alias ID: %s" % data[0]["log"][3]["id"][0]) 174 | click.echo("Alias Email: %s" % data[0]["msg"][1]) 175 | 176 | 177 | @cli.command() 178 | @click.argument('alias_id') 179 | @click.pass_context 180 | def delete(ctx, alias_id): 181 | """Delete a alias.""" 182 | 183 | API_ENDPOINT = "/api/v1/delete/alias" 184 | headers = {'X-API-Key': MAILCOW_API_KEY} 185 | 186 | data = [alias_id] 187 | 188 | try: 189 | r = requests.post(MAILCOW_INSTANCE + API_ENDPOINT, headers=headers, json=data) 190 | r.raise_for_status() 191 | except requests.exceptions.HTTPError as err: 192 | raise SystemExit(err) 193 | 194 | data = r.json() 195 | 196 | click.echo("Success! The following Alias has been deleted:") 197 | click.echo("Alias ID: %s" % data[0]["log"][3]["id"][0]) 198 | click.echo("Alias Email: %s" % data[0]["msg"][1]) 199 | 200 | 201 | def readable_random_string(length: int) -> str: 202 | string = '' 203 | for x in range(int(length / 2)): 204 | string += random.choice(CONSONANTS) 205 | string += random.choice(VOWELS) 206 | return string 207 | 208 | 209 | # Mailcow IPv6 support relies on a docker proxy which in case would nullify the use of the whitelist. 210 | # This patch forces the connection to use IPv4 211 | def allowed_gai_family(): 212 | """ 213 | https://stackoverflow.com/a/46972341 214 | """ 215 | return socket.AF_INET 216 | 217 | 218 | urllib3_cn.allowed_gai_family = allowed_gai_family 219 | 220 | ## Uncomment if you want to use it without installing it 221 | #if __name__ == '__main__': 222 | # cli() 223 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | texttable==1.6.4 2 | click==8.0.1 3 | requests==2.26.0 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='privacycow', 5 | version='0.1.0', 6 | packages=["privacycow"], 7 | include_package_data=True, 8 | install_requires=[ 9 | 'Click==8.0.1', 10 | 'texttable==1.6.4', 11 | 'requests==2.26.0', 12 | ], 13 | package_data={'privacycow': ['config.ini.example']}, 14 | entry_points={ 15 | 'console_scripts': [ 16 | 'privacycow = privacycow.privacycow:cli', 17 | ], 18 | }, 19 | ) 20 | --------------------------------------------------------------------------------