Skip to content

Commit

Permalink
Version 0.3.0
Browse files Browse the repository at this point in the history
What's new:
- Update receiver/sender images to Alpine Linux 3.14
- Use docker-compose for managing deployment
- Use Docker SDK for Python instead of own shell scripts
- Rewrite CLI code
- Use Renovate Bot for updating dependencies
- Update all dependencies
  • Loading branch information
HarrySky committed Aug 24, 2021
1 parent dd0aa0e commit 3517ed8
Show file tree
Hide file tree
Showing 66 changed files with 1,205 additions and 796 deletions.
1 change: 1 addition & 0 deletions .ci/auth-requirements.txt
5 changes: 5 additions & 0 deletions .ci/check_security
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
set -e

bandit --quiet -r kolombo
safety check --full-report
7 changes: 7 additions & 0 deletions .ci/check_style
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
set -e

black --target-version=py39 --check kolombo tests setup.py
mypy kolombo
isort --project=kolombo --check kolombo tests setup.py
flake8 kolombo
2 changes: 2 additions & 0 deletions .ci/security-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bandit==1.7.0
safety==1.10.3
15 changes: 15 additions & 0 deletions .ci/style-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Static type check analysis
mypy==0.910
types-setuptools==57.0.2
# Flake8 with extensions
flake8==3.9.2
flake8-bugbear==21.4.3
flake8-pie==0.14.0
pep8-naming==0.12.1
flake8-simplify==0.14.1
flake8-comprehensions==3.6.1
flake8-fixme==1.1.1
flake8-cognitive-complexity==0.1.0
# Code style enforcing
black==21.6b0
isort==5.9.3
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/venv/
__pycache__/
/*.egg-info/
/.vscode/
__pycache__/
58 changes: 58 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
stages:
- tests

variables:
PIP_CACHE_DIR: "${CI_PROJECT_DIR}/.cache/pip"

cache:
key: "${CI_JOB_STAGE}-${CI_COMMIT_REF_SLUG}-pip"
paths:
- .cache/pip
- venv/

before_script:
- pip install virtualenv && virtualenv venv && source venv/bin/activate

check_style:
stage: tests
tags:
- docker-other
image: python:3.9-slim
script:
- pip install -r requirements.txt
- pip install -r .ci/auth-requirements.txt
- pip install -r .ci/style-requirements.txt
- bash .ci/check_style
only:
refs:
- main
- merge_requests
changes:
- requirements.txt
- setup.py
- kolombo/**/*
- tests/**/*
- .ci/*
- .gitlab-ci.yml

check_security:
stage: tests
tags:
- docker-other
image: python:3.9-slim
script:
- pip install -r requirements.txt
- pip install -r .ci/auth-requirements.txt
- pip install -r .ci/security-requirements.txt
- bash .ci/check_security
only:
refs:
- main
- merge_requests
changes:
- requirements.txt
- setup.py
- kolombo/**/*
- tests/**/*
- .ci/*
- .gitlab-ci.yml
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2020 Igor Nehoroshev
Copyright 2020-now Igor Nehoroshev

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
6 changes: 4 additions & 2 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
include README.md kolombo/py.typed
recursive-include kolombo *.py *.sh *.conf requirements.txt virtual-* Dockerfile
include requirements.txt README.md logo.png kolombo/py.typed
recursive-include kolombo *.py
recursive-include kolombo_auth *.py
recursive-include kolombo/docker *
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
# Kolombo

![Kolombo Logo](https://raw.githubusercontent.com/HarrySky/kolombo/master/logo.png "Kolombo Logo")
![Kolombo Logo](https://raw.githubusercontent.com/HarrySky/kolombo/main/logo.png "Kolombo Logo")

Easy to manage mail server 💌
CLI for easy mail server managing 💌

**NB! Work in progress, not ready!**
**NB! Work in progress, not ready for production use!**

## Introduction
## How to use

TODO
Documentation is coming, for now,
this is how to setup mail server for
domain example.com with email info@example.com

```bash
# Initialize Kolombo
kolombo init

# Add domain and generate DKIM keys for it
kolombo domain add example.com mx.example.com # MX field is optional
kolombo dkim generate example.com # generates DKIM keys and returns DNS TXT record to add
kolombo dkim txt example.com # returns DNS TXT record to add

# Add user (email) for domain you just added
kolombo user add info@example.com

# Deploy Kolombo services one by one...

kolombo run receiver # Listens on 25 for incoming mail, gives emails to users that come through nginx 993/995 port
kolombo run auth # Authenticates SMTP/POP3/IMAP users from nginx
kolombo run nginx # Listens on 465 (SMTP), 993 (IMAP) and 995 (POP3)
kolombo run senders # Sends emails that come through nginx 465 port
```
6 changes: 1 addition & 5 deletions kolombo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
from kolombo.auth import api
from kolombo.bin import main as cli

__version__ = "0.2.0"
__all__ = ["api", "cli"]
__version__ = "0.3.0"
1 change: 0 additions & 1 deletion kolombo/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
from kolombo.auth.api import api # noqa: F401
2 changes: 1 addition & 1 deletion kolombo/auth/_resolver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from socket import gethostbyname


def get_ip_by_host(host: str) -> str:
def get_ip_for_host(host: str) -> str:
"""Return IP address for provided host (fast in Docker network)"""
return gethostbyname(host)
12 changes: 7 additions & 5 deletions kolombo/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

from kolombo import conf
from kolombo.auth.endpoints import auth
from kolombo.resources import init_database, init_logger
from kolombo.models import init_database

api = FastAPI(
conf.read_configuration()
app = FastAPI(
title="Kolombo auth API",
debug=conf.DEBUG,
on_startup=[init_logger, init_database],
# Disable Swagger UI and other docs
on_startup=[init_database],
# Disable Swagger UI and other API docs
openapi_url=None,
docs_url=None,
redoc_url=None,
)
api.add_api_route("/auth", auth, methods=["GET"])
app.add_api_route("/auth", auth, methods=["GET"])
6 changes: 3 additions & 3 deletions kolombo/auth/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from ormar.exceptions import NoMatch

from kolombo import conf
from kolombo.console import debug
from kolombo.models import User
from kolombo.resources import log


async def check_credentials(email: str, password: str, domain: str) -> bool:
Expand All @@ -15,11 +15,11 @@ async def check_credentials(email: str, password: str, domain: str) -> bool:
user = await User.objects.filter(active=True, email=email, domain=domain).get()
expected_key = b64decode(user.password)
except NoMatch:
log.debug("No active user '%s' from domain '%s' found", email, domain)
debug(f"No active user '{email}' from domain '{domain}' found")
key_material = b"Password that 100% not matching, I swear"
expected_key = b"Password that 100% not matching, trust me"

kdf = Scrypt(conf.SALT, length=32, n=2 ** 16, r=8, p=1) # type: ignore[call-arg]
kdf = Scrypt(conf.SALT, length=32, n=2 ** 16, r=8, p=1)
try:
kdf.verify(key_material, expected_key)
return True
Expand Down
23 changes: 14 additions & 9 deletions kolombo/auth/endpoints.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from datetime import datetime

from fastapi import Header, Response

from kolombo import conf
from kolombo.auth._resolver import get_ip_by_host
from kolombo.auth._resolver import get_ip_for_host
from kolombo.auth.credentials import check_credentials
from kolombo.auth.protocol import AuthError, AuthSuccess
from kolombo.resources import log
from kolombo.console import debug, error, info, warning

#: We only put SMTP-send and IMAP behind auth, POP3 is unsupported (and old)
_protocol_to_port_mapping = {"smtp": 25, "imap": 143}
#: We only put SMTP-send, IMAP and POP3 (for GMail. Google, use IMAP in 2021 ffs!)
_protocol_to_port_mapping = {"smtp": 25, "imap": 143, "pop3": 110}


async def auth(
Expand All @@ -21,31 +23,34 @@ async def auth(
client_ip: str = Header(...), # noqa: B008
) -> Response:
"""Endpoint used for auth of SMTP/IMAP users."""
now = datetime.now().isoformat()
if x_secret_key != conf.NGINX_SECRET_KEY:
# This MUST NOT happen if everything is set up properly
log.error("Not nginx trying to use API")
error(f"({now}) Not nginx trying to use API")
return AuthError("Go Away", retry=False)

# Check for possible usage errors to close connection early
if protocol not in _protocol_to_port_mapping:
error(f"({now}) Unsupported protocol: {protocol}")
return AuthError("Unsupported protocol", retry=False)
elif auth_method != "plain":
error(f"({now}) Unsupported auth method: {auth_method}")
return AuthError("Unsupported auth method", retry=False)

# Remove newline from credentials
email = email.rstrip("%0A")
password = password.rstrip("%0A")
if not await check_credentials(email, password, domain):
log.warning("Failed %s auth as %s from %s", protocol, email, client_ip)
warning(f"({now}) Failed {protocol} auth as {email} from {client_ip}")
retry = login_attempt < conf.MAX_ATTEMPTS
return AuthError("Invalid login or password", retry=retry)

log.info("Successful %s auth as %s from %s", protocol, email, client_ip)
info(f"({now}) Successful {protocol} auth as {email} from {client_ip}")
server_host = "kolombo-receiver"
if protocol == "smtp":
server_host = f"kolombo-{domain}-sender"

server_ip = get_ip_by_host(server_host)
server_ip = get_ip_for_host(server_host)
server_port = _protocol_to_port_mapping[protocol]
log.debug("Forwarding nginx to %s:%s (%s)", server_ip, server_port, server_host)
debug(f"({now}) Forwarding nginx to {server_ip}:{server_port} ({server_host})")
return AuthSuccess(server_ip, server_port)
16 changes: 8 additions & 8 deletions kolombo/auth/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class AuthSuccess(Response):
"""
Response that MUST be returned after successful auth.
**Parameters:**
Parameters:
* **server_ip** - IP that nginx will use to access real SMTP/IMAP server.
* **server_port** - port that nginx will use to access real SMTP/IMAP server.
* server_ip - IP that nginx will use to access real SMTP/IMAP server.
* server_port - port that nginx will use to access real SMTP/IMAP server.
"""

def __init__(self, server_ip: str, server_port: int) -> None:
Expand All @@ -28,13 +28,13 @@ class AuthError(Response):
"""
Response that MUST be returned when auth error occured.
**Parameters:**
Parameters:
* **status** - status that explains error that happened.
* **retry** - *(optional)* whether client can retry (default behaviour)
* status - status that explains error that happened.
* retry - *(optional)* whether client can retry (default behaviour)
or connection will be closed.
* **wait** - *(optional)* seconds to wait if client can retry (default - 2).
* **error_code** - *(optional)* error code that will be used in SMTP case
* wait - *(optional)* seconds to wait if client can retry (default - 2).
* error_code - *(optional)* error code that will be used in SMTP case
(default - "535 5.7.0", a.k.a. "Authentication Failed").
"""

Expand Down
33 changes: 32 additions & 1 deletion kolombo/bin/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
from kolombo.bin.kolombo import main # noqa: F401
from os import path

from typer import Context, Typer

from kolombo import conf
from kolombo.console import error
from kolombo.dkim import dkim_cli
from kolombo.domain import domain_cli
from kolombo.init import init
from kolombo.run import run_cli
from kolombo.stop import stop_cli
from kolombo.user import user_cli

kolombo_cli = Typer(name="kolombo", add_completion=True)
kolombo_cli.command("init")(init)
kolombo_cli.add_typer(domain_cli, name="domain")
kolombo_cli.add_typer(dkim_cli, name="dkim")
kolombo_cli.add_typer(user_cli, name="user")
kolombo_cli.add_typer(run_cli, name="run")
kolombo_cli.add_typer(stop_cli, name="stop")


@kolombo_cli.callback()
def main(ctx: Context) -> None:
if ctx.invoked_subcommand == "init":
return

if not path.exists("/etc/kolombo/kolombo.conf"):
error("Kolombo is not initialized! Run [code]kolombo init[/] first")
exit(1)

conf.read_configuration()
Loading

0 comments on commit 3517ed8

Please sign in to comment.