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!
|
||||
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
|
||||
|
|
|
@ -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!
|
|
@ -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
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 .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)
|
||||
|
|
Loading…
Reference in a new issue