diff --git a/README.md b/README.md index ed08ec0..62cf95a 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,19 @@ pending_lifetime: 604800 # HTTPS-terminating reverse proxy! host: 127.0.0.1 port: 8080 +# Defaults to stdout, supported: stdout, smtp +mailing_method: smtp +# Configure smtp client options +smtp: + # Connect to this SMTP server to send a mail. + hostname: localhost + port: 25 + # if tls=True, starttls is ignored + tls: false + starttls: false + # Omit username/password if authentication is not needed. + username: webkey + password: SuperS3curePassword123 # Every domain served by EasyWKS must be listed here domains: example.org: @@ -149,20 +162,15 @@ Note that the webserver built into EasyWKS validates that the `?l=` matches #### Postfix -Create a small wrapper script, e.g. `/usr/local/bin/postfix-easywks`: - -```shell -#!/bin/bash -/path/to/easywks process | /usr/sbin/sendmail -t -``` - Add an entry in `/etc/postfix/master.cf`: ``` webkey unix - n n - - pipe - flags=DRhu user=webkey argv=/usr/local/bin/postfix-easywks + flags=DRhu user=webkey argv=/path/to/easywks process ``` +Configure EasyWKS to send outgoing mails via SMTP. + [wkd]: https://wiki.gnupg.org/WKD [wks]: https://wiki.gnupg.org/WKS diff --git a/ROADMAP.md b/ROADMAP.md index 6d953d9..5e43ba1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,5 @@ # EasyWKS Roadmap -- [ ] Don't rely on sendmail to send out responses. Use e.g. `smtplib`. - - [ ] Get rid of postfix-easywks wrapper script. - [ ] LMTP server mode, e.g. using the `smtpd` module. -- [ ] Figure out whether file locking in the working directory is necessary to avoid races. \ No newline at end of file +- [ ] Figure out whether file locking in the working directory is necessary to avoid races. +- [ ] Testing, testing, testing! \ No newline at end of file diff --git a/easywks/config.py b/easywks/config.py index 9986c9b..261029e 100644 --- a/easywks/config.py +++ b/easywks/config.py @@ -3,18 +3,51 @@ import string import yaml +def _validate_mailing_method(value): + methods = ['stdout', 'smtp'] + if value not in methods: + return f'permitted values: {methods}, got {value}' + + +def _validate_smtp_config(value): + if not isinstance(value, dict): + return f'must be a map, got {type(value)}' + if not isinstance(value['host'], str): + return f'host must be a str, got {type(value["host"])}' + if not isinstance(value['port'], int): + return f'port must be a int, got {type(value["port"])}' + if 'tls' in value: + if not isinstance(value['tls'], bool): + return f'tls must be a bool, got {type(value["tls"])}' + else: + value['tls'] = False + if 'starttls' in value: + if not isinstance(value['starttls'], bool): + return f'starttls must be a bool, got {type(value["starttls"])}' + else: + value['starttls'] = False + if 'username' in value and 'password' in value: + if not isinstance(value['username'], str): + return f'username must be a str, got {type(value["username"])}' + if not isinstance(value['password'], str): + return f'password must be a str, got {type(value["password"])}' + else: + value['username'] = None + value['password'] = None + + def _validate_policy_flags(value): alphabet = string.ascii_lowercase + string.digits + '-._' if not isinstance(value, dict): - return 'policy_flags must be a map' + return f'must be a map, got {type(value)}' for flag, v in value.items(): if not isinstance(flag, str) or len(flag) == 0: - return 'policy_flags has non-string or empty members' + return 'has non-string or empty members' if flag[0] not in string.ascii_lowercase: - return 'policy_flags must start with a lowercase letter' + return 'must start with a lowercase letter' for c in flag: if c not in alphabet: - return f'policy_flags has invalid key {flag}' + return f'has invalid key {flag}' class _ConfigOption: @@ -87,5 +120,12 @@ Config = _GlobalConfig( working_directory=_ConfigOption('directory', str, '/var/lib/easywks'), host=_ConfigOption('host', str, '127.0.0.1'), port=_ConfigOption('port', int, 8080), - pending_lifetime=_ConfigOption('pending_lifetime', int, 604800) + pending_lifetime=_ConfigOption('pending_lifetime', int, 604800), + mailing_method=_ConfigOption('mailing_method', str, 'stdout', validator=_validate_mailing_method), + smtp=_ConfigOption('smtp', dict, { + 'host': 'localhost', + 'port': 25, + 'tls': False, + 'starttls': False + }, validator=_validate_smtp_config), ) diff --git a/easywks/mailing.py b/easywks/mailing.py new file mode 100644 index 0000000..a182401 --- /dev/null +++ b/easywks/mailing.py @@ -0,0 +1,38 @@ + +import smtplib +import email.policy +from email.message import EmailMessage + +from .config import Config + + +def _stdout_mailing(message: EmailMessage): + print(message.as_string(policy=email.policy.default)) + + +def _smtp_mailing(message: EmailMessage): + conf = Config.smtp + cls = smtplib.SMTP + if conf['tls']: + cls = smtplib.SMTP_SSL + with cls(conf['host'], conf['port']) as smtp: + if not conf['tls'] and conf['starttls']: + smtp.starttls() + if conf['username'] is not None: + smtp.login(conf['username'], conf['password']) + smtp.send_message(message) + smtp.quit() + + +_mailing_methods = { + 'stdout': _stdout_mailing, + 'smtp': _smtp_mailing, +} + + +def list_mailing_methods(): + return list(_mailing_methods.keys()) + + +def get_mailing_method(method: str): + return _mailing_methods.get(method) diff --git a/easywks/process.py b/easywks/process.py index 2fb2604..20ec04f 100644 --- a/easywks/process.py +++ b/easywks/process.py @@ -2,6 +2,7 @@ import sys from typing import List, Dict from .crypto import pgp_decrypt +from .mailing import get_mailing_method from .util import xloop_header from .config import Config from .files import read_pending_key, write_public_key, remove_pending_key, write_pending_key @@ -133,6 +134,7 @@ def process_mail(mail: bytes = None): leafs = _get_mime_leafs(msg) pgp: PGPMessage = _get_pgp_message(leafs) decrypted = pgp_decrypt(sender_domain, pgp) + rmsg = None if decrypted.is_signed: request: ConfirmationResponse = _parse_confirmation_response(decrypted, submission_address, sender_mail) try: @@ -142,13 +144,15 @@ def process_mail(mail: bytes = None): return # this throws an error if signature verification fails response: PublishResponse = request.verify_signature(key) - rmsg = response.create_signed_message().as_string(policy=SMTP) + rmsg = response.create_signed_message() write_public_key(sender_domain, sender_mail, key) remove_pending_key(sender_domain, request.nonce) - print(rmsg) else: request: SubmissionRequest = _parse_submission_request(decrypted, submission_address, sender_mail) response: ConfirmationRequest = request.confirmation_request() - rmsg = response.create_signed_message().as_string(policy=SMTP) + rmsg = response.create_signed_message() write_pending_key(sender_domain, response.nonce, request.key) - print(rmsg) + # Finally send out the response + if rmsg: + method = get_mailing_method(Config.mailing_method) + method(rmsg)