Add SMTP sending mode

This commit is contained in:
s3lph 2021-09-26 11:12:01 +02:00
parent ac660802a4
commit 6520ba29c5
5 changed files with 109 additions and 20 deletions

View file

@ -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=<uid>` 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

View file

@ -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.
- [ ] Figure out whether file locking in the working directory is necessary to avoid races.
- [ ] Testing, testing, testing!

View file

@ -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),
)

38
easywks/mailing.py Normal file
View file

@ -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)

View file

@ -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)