Add SMTP sending mode
This commit is contained in:
parent
ac660802a4
commit
6520ba29c5
5 changed files with 109 additions and 20 deletions
24
README.md
24
README.md
|
@ -85,6 +85,19 @@ pending_lifetime: 604800
|
||||||
# HTTPS-terminating reverse proxy!
|
# HTTPS-terminating reverse proxy!
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 8080
|
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
|
# Every domain served by EasyWKS must be listed here
|
||||||
domains:
|
domains:
|
||||||
example.org:
|
example.org:
|
||||||
|
@ -149,20 +162,15 @@ Note that the webserver built into EasyWKS validates that the `?l=<uid>` matches
|
||||||
|
|
||||||
#### Postfix
|
#### 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`:
|
Add an entry in `/etc/postfix/master.cf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
webkey unix - n n - - pipe
|
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
|
[wkd]: https://wiki.gnupg.org/WKD
|
||||||
[wks]: https://wiki.gnupg.org/WKS
|
[wks]: https://wiki.gnupg.org/WKS
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# EasyWKS Roadmap
|
# 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.
|
- [ ] 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!
|
|
@ -3,18 +3,51 @@ import string
|
||||||
import yaml
|
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):
|
def _validate_policy_flags(value):
|
||||||
alphabet = string.ascii_lowercase + string.digits + '-._'
|
alphabet = string.ascii_lowercase + string.digits + '-._'
|
||||||
if not isinstance(value, dict):
|
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():
|
for flag, v in value.items():
|
||||||
if not isinstance(flag, str) or len(flag) == 0:
|
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:
|
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:
|
for c in flag:
|
||||||
if c not in alphabet:
|
if c not in alphabet:
|
||||||
return f'policy_flags has invalid key {flag}'
|
return f'has invalid key {flag}'
|
||||||
|
|
||||||
|
|
||||||
class _ConfigOption:
|
class _ConfigOption:
|
||||||
|
@ -87,5 +120,12 @@ Config = _GlobalConfig(
|
||||||
working_directory=_ConfigOption('directory', str, '/var/lib/easywks'),
|
working_directory=_ConfigOption('directory', str, '/var/lib/easywks'),
|
||||||
host=_ConfigOption('host', str, '127.0.0.1'),
|
host=_ConfigOption('host', str, '127.0.0.1'),
|
||||||
port=_ConfigOption('port', int, 8080),
|
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
38
easywks/mailing.py
Normal 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)
|
|
@ -2,6 +2,7 @@ import sys
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
from .crypto import pgp_decrypt
|
from .crypto import pgp_decrypt
|
||||||
|
from .mailing import get_mailing_method
|
||||||
from .util import xloop_header
|
from .util import xloop_header
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .files import read_pending_key, write_public_key, remove_pending_key, write_pending_key
|
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)
|
leafs = _get_mime_leafs(msg)
|
||||||
pgp: PGPMessage = _get_pgp_message(leafs)
|
pgp: PGPMessage = _get_pgp_message(leafs)
|
||||||
decrypted = pgp_decrypt(sender_domain, pgp)
|
decrypted = pgp_decrypt(sender_domain, pgp)
|
||||||
|
rmsg = None
|
||||||
if decrypted.is_signed:
|
if decrypted.is_signed:
|
||||||
request: ConfirmationResponse = _parse_confirmation_response(decrypted, submission_address, sender_mail)
|
request: ConfirmationResponse = _parse_confirmation_response(decrypted, submission_address, sender_mail)
|
||||||
try:
|
try:
|
||||||
|
@ -142,13 +144,15 @@ def process_mail(mail: bytes = None):
|
||||||
return
|
return
|
||||||
# this throws an error if signature verification fails
|
# this throws an error if signature verification fails
|
||||||
response: PublishResponse = request.verify_signature(key)
|
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)
|
write_public_key(sender_domain, sender_mail, key)
|
||||||
remove_pending_key(sender_domain, request.nonce)
|
remove_pending_key(sender_domain, request.nonce)
|
||||||
print(rmsg)
|
|
||||||
else:
|
else:
|
||||||
request: SubmissionRequest = _parse_submission_request(decrypted, submission_address, sender_mail)
|
request: SubmissionRequest = _parse_submission_request(decrypted, submission_address, sender_mail)
|
||||||
response: ConfirmationRequest = request.confirmation_request()
|
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)
|
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)
|
||||||
|
|
Loading…
Reference in a new issue