mirror of
https://github.com/chatmail/relay.git
synced 2026-05-20 12:58:04 +00:00
Reject unencrypted incoming mail (#538)
* draft blocking of incoming non-encrypted mail * create a new enforceE2EE file in address dirs by default and only accept incoming cleartext file if the enforceE2EE file is missing * Update cmdeploy/src/cmdeploy/service/filtermail.service.f Co-authored-by: l <link2xt@testrun.org> * fix benchmark so they setup encryption * hack around limitations of aiosmtpd's handliung of RCPTO options * add tests, and split incoming/outgoing handlers for clarity * document mailbox directory structure, some streamlining of features/E2EE in intro * use SMTP response code "523 Encryption Needed" * filtermail: care for the case that the recipient does not exist Co-authored-by: missytake <missytake@systemli.org> * Update chatmaild/src/chatmaild/filtermail.py Co-authored-by: l <link2xt@testrun.org> * Update chatmaild/src/chatmaild/filtermail.py Co-authored-by: l <link2xt@testrun.org> * remove debug info print * ensure multipart/report type for mailer-daemon messages * Allow sending out Autocrypt Setup Messages --------- Co-authored-by: l <link2xt@testrun.org> Co-authored-by: missytake <missytake@systemli.org>
This commit is contained in:
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
|
- Enforce end-to-end encryption for incoming messages.
|
||||||
|
New user address mailboxes now get a `enforceE2EEincoming` file
|
||||||
|
which prohibits incoming cleartext messages from other domains.
|
||||||
|
An outside MTA trying to submit a cleartext message will
|
||||||
|
get a "523 Encryption Needed" response, see RFC5248.
|
||||||
|
If the file does not exist (as it the case for all existing accounts)
|
||||||
|
incoming cleartext messages are accepted.
|
||||||
|
([#538](https://github.com/chatmail/server/pull/538))
|
||||||
|
|
||||||
- Enforce end-to-end encryption between local addresses
|
- Enforce end-to-end encryption between local addresses
|
||||||
([#535](https://github.com/chatmail/server/pull/535))
|
([#535](https://github.com/chatmail/server/pull/535))
|
||||||
|
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -1,20 +1,23 @@
|
|||||||
|
|
||||||
<img width="800px" src="www/src/collage-top.png"/>
|
<img width="800px" src="www/src/collage-top.png"/>
|
||||||
|
|
||||||
# Chatmail servers for secure instant messaging
|
# Chatmail servers for end-to-end encrypted instant messaging
|
||||||
|
|
||||||
Chatmail servers are interoperable email routing machines optimized for:
|
Chatmail servers are interoperable e-mail routing machines optimized for
|
||||||
|
|
||||||
- **Convenience:** Low friction instant onboarding
|
- **Convenience:** Low friction instant onboarding
|
||||||
|
|
||||||
- **Privacy:** No name, phone numbers, email required or collected
|
- **Privacy:** No name, phone numbers, email required or collected
|
||||||
|
|
||||||
|
- **End-to-End Encryption enforced**: only OpenPGP messages with metadata minimization allowed
|
||||||
|
|
||||||
- **Instant:** Privacy-preserving push notifications for Apple, Google, and Huawei
|
- **Instant:** Privacy-preserving push notifications for Apple, Google, and Huawei
|
||||||
|
|
||||||
- **Speed:** Message delivery in well under a second.
|
- **Speed:** Message delivery in well under a second.
|
||||||
|
|
||||||
- **Security:** Strict TLS, DKIM and OpenPGP with metadata-minimization rules enforced.
|
- **Transport Security:** Strict TLS and DKIM enforced.
|
||||||
|
|
||||||
- **Reliability:** No spam or IP reputation checks, rate-limits suitable for realtime chats.
|
- **Reliability:** No spam or IP reputation checks; rate-limits are suitable for realtime chats.
|
||||||
|
|
||||||
- **Efficiency:** Messages are only stored for transit and removed automatically.
|
- **Efficiency:** Messages are only stored for transit and removed automatically.
|
||||||
|
|
||||||
@@ -191,9 +194,9 @@ A short overview:
|
|||||||
to authenticate users
|
to authenticate users
|
||||||
to send mails for them.
|
to send mails for them.
|
||||||
|
|
||||||
- [`filtermail`](https://github.com/chatmail/server/blob/main/chatmaild/src/chatmaild/filtermail.py) prevents
|
- [`filtermail`](https://github.com/chatmail/server/blob/main/chatmaild/src/chatmaild/filtermail.py)
|
||||||
unencrypted email from leaving the chatmail service
|
prevents unencrypted email from leaving or entering the chatmail service
|
||||||
and is integrated into Postfix's outbound mail pipelines.
|
and is integrated into Postfix's outbound and inbound mail pipelines.
|
||||||
|
|
||||||
- [`chatmail-metadata`](https://github.com/chatmail/server/blob/main/chatmaild/src/chatmaild/metadata.py) is contacted by a
|
- [`chatmail-metadata`](https://github.com/chatmail/server/blob/main/chatmaild/src/chatmaild/metadata.py) is contacted by a
|
||||||
[dovecot lua script](https://github.com/chatmail/server/blob/main/cmdeploy/src/cmdeploy/dovecot/push_notification.lua)
|
[dovecot lua script](https://github.com/chatmail/server/blob/main/cmdeploy/src/cmdeploy/dovecot/push_notification.lua)
|
||||||
@@ -237,7 +240,6 @@ by the according markdown `.md` file in the `www/src` directory.
|
|||||||
|
|
||||||
### Refining the web pages
|
### Refining the web pages
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy webdev
|
scripts/cmdeploy webdev
|
||||||
```
|
```
|
||||||
@@ -252,6 +254,23 @@ This starts a local live development cycle for chatmail web pages:
|
|||||||
|
|
||||||
- Starts a browser window automatically where you can "refresh" as needed.
|
- Starts a browser window automatically where you can "refresh" as needed.
|
||||||
|
|
||||||
|
## Mailbox directory layout
|
||||||
|
|
||||||
|
Fresh chatmail server addresses have a mailbox directory that contains:
|
||||||
|
|
||||||
|
- a `password` file with the salted password required for authenticating
|
||||||
|
whether a login may use the address to send/receive messages.
|
||||||
|
If you modify the password file manually, you effectively block the user.
|
||||||
|
|
||||||
|
- `enforceE2EEincoming` is a default-created file with each address.
|
||||||
|
If present the file indicates that this chatmail address rejects incoming cleartext messages.
|
||||||
|
If absent the address accepts incoming cleartext messages.
|
||||||
|
|
||||||
|
- `dovecot*`, `cur`, `new` and `tmp` represent IMAP/mailbox state.
|
||||||
|
If the address is only used by one device, the Maildir directories
|
||||||
|
will typically be empty unless the user of that address hasn't been online
|
||||||
|
for a while.
|
||||||
|
|
||||||
|
|
||||||
## Emergency Commands to disable automatic account creation
|
## Emergency Commands to disable automatic account creation
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ class Config:
|
|||||||
self.passthrough_senders = params["passthrough_senders"].split()
|
self.passthrough_senders = params["passthrough_senders"].split()
|
||||||
self.passthrough_recipients = params["passthrough_recipients"].split()
|
self.passthrough_recipients = params["passthrough_recipients"].split()
|
||||||
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
||||||
|
self.filtermail_smtp_port_incoming = int(
|
||||||
|
params["filtermail_smtp_port_incoming"]
|
||||||
|
)
|
||||||
self.postfix_reinject_port = int(params["postfix_reinject_port"])
|
self.postfix_reinject_port = int(params["postfix_reinject_port"])
|
||||||
|
self.postfix_reinject_port_incoming = int(
|
||||||
|
params["postfix_reinject_port_incoming"]
|
||||||
|
)
|
||||||
self.mtail_address = params.get("mtail_address")
|
self.mtail_address = params.get("mtail_address")
|
||||||
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
|
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
|
||||||
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ from email.utils import parseaddr
|
|||||||
from smtplib import SMTP as SMTPClient
|
from smtplib import SMTP as SMTPClient
|
||||||
|
|
||||||
from aiosmtpd.controller import Controller
|
from aiosmtpd.controller import Controller
|
||||||
|
from aiosmtpd.smtp import SMTP
|
||||||
|
|
||||||
from .config import read_config
|
from .config import read_config
|
||||||
|
|
||||||
|
ENCRYPTION_NEEDED_523 = "523 Encryption Needed: Invalid Unencrypted Mail"
|
||||||
|
|
||||||
|
|
||||||
def check_openpgp_payload(payload: bytes):
|
def check_openpgp_payload(payload: bytes):
|
||||||
"""Checks the OpenPGP payload.
|
"""Checks the OpenPGP payload.
|
||||||
@@ -157,9 +160,14 @@ def check_encrypted(message):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def asyncmain_beforequeue(config):
|
async def asyncmain_beforequeue(config, mode):
|
||||||
port = config.filtermail_smtp_port
|
if mode == "outgoing":
|
||||||
Controller(BeforeQueueHandler(config), hostname="127.0.0.1", port=port).start()
|
port = config.filtermail_smtp_port
|
||||||
|
handler = OutgoingBeforeQueueHandler(config)
|
||||||
|
else:
|
||||||
|
port = config.filtermail_smtp_port_incoming
|
||||||
|
handler = IncomingBeforeQueueHandler(config)
|
||||||
|
HackedController(handler, hostname="127.0.0.1", port=port).start()
|
||||||
|
|
||||||
|
|
||||||
def recipient_matches_passthrough(recipient, passthrough_recipients):
|
def recipient_matches_passthrough(recipient, passthrough_recipients):
|
||||||
@@ -171,7 +179,21 @@ def recipient_matches_passthrough(recipient, passthrough_recipients):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BeforeQueueHandler:
|
class HackedController(Controller):
|
||||||
|
def factory(self):
|
||||||
|
return SMTPDiscardRCPTO_options(self.handler, **self.SMTP_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SMTPDiscardRCPTO_options(SMTP):
|
||||||
|
def _getparams(self, params):
|
||||||
|
# aiosmtpd's SMTP daemon fails to handle a request if there are RCPT TO options
|
||||||
|
# We just ignore them for our incoming filtermail purposes
|
||||||
|
if len(params) == 1 and params[0].startswith("ORCPT"):
|
||||||
|
return {}
|
||||||
|
return super()._getparams(params)
|
||||||
|
|
||||||
|
|
||||||
|
class OutgoingBeforeQueueHandler:
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.send_rate_limiter = SendRateLimiter()
|
self.send_rate_limiter = SendRateLimiter()
|
||||||
@@ -210,28 +232,85 @@ class BeforeQueueHandler:
|
|||||||
|
|
||||||
_, from_addr = parseaddr(message.get("from").strip())
|
_, from_addr = parseaddr(message.get("from").strip())
|
||||||
|
|
||||||
logging.info(f"mime-from: {from_addr} envelope-from: {envelope.mail_from!r}")
|
|
||||||
if envelope.mail_from.lower() != from_addr.lower():
|
if envelope.mail_from.lower() != from_addr.lower():
|
||||||
return f"500 Invalid FROM <{from_addr!r}> for <{envelope.mail_from!r}>"
|
return f"500 Invalid FROM <{from_addr!r}> for <{envelope.mail_from!r}>"
|
||||||
|
|
||||||
if mail_encrypted:
|
if mail_encrypted or is_securejoin(message):
|
||||||
print("Filtering encrypted mail.", file=sys.stderr)
|
print("Outgoing: Filtering encrypted mail.", file=sys.stderr)
|
||||||
else:
|
return
|
||||||
print("Filtering unencrypted mail.", file=sys.stderr)
|
|
||||||
|
print("Outgoing: Filtering unencrypted mail.", file=sys.stderr)
|
||||||
|
|
||||||
if envelope.mail_from in self.config.passthrough_senders:
|
if envelope.mail_from in self.config.passthrough_senders:
|
||||||
return
|
return
|
||||||
|
|
||||||
if mail_encrypted or is_securejoin(message):
|
# allow self-sent Autocrypt Setup Message
|
||||||
return
|
if envelope.rcpt_tos == [from_addr]:
|
||||||
|
if message.get("subject") == "Autocrypt Setup Message":
|
||||||
|
if message.get_content_type() == "multipart/mixed":
|
||||||
|
return
|
||||||
|
|
||||||
passthrough_recipients = self.config.passthrough_recipients
|
passthrough_recipients = self.config.passthrough_recipients
|
||||||
|
|
||||||
for recipient in envelope.rcpt_tos:
|
for recipient in envelope.rcpt_tos:
|
||||||
if recipient_matches_passthrough(recipient, passthrough_recipients):
|
if recipient_matches_passthrough(recipient, passthrough_recipients):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print("Rejected unencrypted mail.", file=sys.stderr)
|
print("Rejected unencrypted mail.", file=sys.stderr)
|
||||||
return "500 Invalid unencrypted mail"
|
return ENCRYPTION_NEEDED_523
|
||||||
|
|
||||||
|
|
||||||
|
class IncomingBeforeQueueHandler:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
async def handle_DATA(self, server, session, envelope):
|
||||||
|
logging.info("handle_DATA before-queue")
|
||||||
|
error = self.check_DATA(envelope)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
logging.info("re-injecting the mail that passed checks")
|
||||||
|
|
||||||
|
# the smtp daemon on reinject_port_incoming gives it to dkim milter
|
||||||
|
# which looks at source address to determine whether to verify or sign
|
||||||
|
client = SMTPClient(
|
||||||
|
"localhost",
|
||||||
|
self.config.postfix_reinject_port_incoming,
|
||||||
|
source_address=("127.0.0.2", 0),
|
||||||
|
)
|
||||||
|
client.sendmail(
|
||||||
|
envelope.mail_from, envelope.rcpt_tos, envelope.original_content
|
||||||
|
)
|
||||||
|
return "250 OK"
|
||||||
|
|
||||||
|
def check_DATA(self, envelope):
|
||||||
|
"""the central filtering function for e-mails."""
|
||||||
|
logging.info(f"Processing DATA message from {envelope.mail_from}")
|
||||||
|
|
||||||
|
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
|
||||||
|
mail_encrypted = check_encrypted(message)
|
||||||
|
|
||||||
|
if mail_encrypted or is_securejoin(message):
|
||||||
|
print("Incoming: Filtering encrypted mail.", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Incoming: Filtering unencrypted mail.", file=sys.stderr)
|
||||||
|
|
||||||
|
# we want cleartext mailer-daemon messages to pass through
|
||||||
|
# chatmail core will typically not display them as normal messages
|
||||||
|
if message.get("auto-submitted"):
|
||||||
|
_, from_addr = parseaddr(message.get("from").strip())
|
||||||
|
if from_addr.lower().startswith("mailer-daemon@"):
|
||||||
|
if message.get_content_type() == "multipart/report":
|
||||||
|
return
|
||||||
|
|
||||||
|
for recipient in envelope.rcpt_tos:
|
||||||
|
user = self.config.get_user(recipient)
|
||||||
|
if user is None or user.is_incoming_cleartext_ok():
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("Rejected unencrypted mail.", file=sys.stderr)
|
||||||
|
return ENCRYPTION_NEEDED_523
|
||||||
|
|
||||||
|
|
||||||
class SendRateLimiter:
|
class SendRateLimiter:
|
||||||
@@ -250,11 +329,14 @@ class SendRateLimiter:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
assert len(args) == 1
|
assert len(args) == 2
|
||||||
config = read_config(args[0])
|
config = read_config(args[0])
|
||||||
|
mode = args[1]
|
||||||
logging.basicConfig(level=logging.WARN)
|
logging.basicConfig(level=logging.WARN)
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
task = asyncmain_beforequeue(config)
|
assert mode in ["incoming", "outgoing"]
|
||||||
|
task = asyncmain_beforequeue(config, mode)
|
||||||
loop.create_task(task)
|
loop.create_task(task)
|
||||||
|
logging.info("entering serving loop")
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|||||||
@@ -46,12 +46,14 @@ passthrough_recipients = xstore@testrun.org
|
|||||||
# Deployment Details
|
# Deployment Details
|
||||||
#
|
#
|
||||||
|
|
||||||
# where the filtermail SMTP service listens
|
# SMTP outgoing filtermail and reinjection
|
||||||
filtermail_smtp_port = 10080
|
filtermail_smtp_port = 10080
|
||||||
|
|
||||||
# postfix accepts on the localhost reinject SMTP port
|
|
||||||
postfix_reinject_port = 10025
|
postfix_reinject_port = 10025
|
||||||
|
|
||||||
|
# SMTP incoming filtermail and reinjection
|
||||||
|
filtermail_smtp_port_incoming = 10081
|
||||||
|
postfix_reinject_port_incoming = 10026
|
||||||
|
|
||||||
# if set to "True" IPv6 is disabled
|
# if set to "True" IPv6 is disabled
|
||||||
disable_ipv6 = False
|
disable_ipv6 = False
|
||||||
|
|
||||||
|
|||||||
56
chatmaild/src/chatmaild/tests/mail-data/asm.eml
Normal file
56
chatmaild/src/chatmaild/tests/mail-data/asm.eml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
From: {from_addr}
|
||||||
|
To: {to_addr}
|
||||||
|
Autocrypt-Setup-Message: v1
|
||||||
|
Subject: Autocrypt Setup Message
|
||||||
|
Date: Tue, 22 Jan 2019 12:56:29 +0100
|
||||||
|
Content-type: multipart/mixed; boundary="Y6fyGi9SoGeH8WwRaEdC6bbBcYOedDzrQ"
|
||||||
|
|
||||||
|
--Y6fyGi9SoGeH8WwRaEdC6bbBcYOedDzrQ
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
This message contains all information to transfer your Autocrypt
|
||||||
|
settings along with your secret key securely from your original
|
||||||
|
device.
|
||||||
|
|
||||||
|
To set up your new device for Autocrypt, please follow the
|
||||||
|
instuctions that should be presented by your new device.
|
||||||
|
|
||||||
|
You can keep this message and use it as a backup for your secret
|
||||||
|
key. If you want to do this, you should write down the Setup Code
|
||||||
|
and store it securely.
|
||||||
|
--Y6fyGi9SoGeH8WwRaEdC6bbBcYOedDzrQ
|
||||||
|
Content-Type: application/autocrypt-setup
|
||||||
|
Content-Disposition: attachment; filename="autocrypt-setup-message.html"
|
||||||
|
|
||||||
|
<html><body>
|
||||||
|
<p>
|
||||||
|
This is the Autocrypt setup file used to transfer settings and
|
||||||
|
keys between clients. You can decrypt it using the Setup Code
|
||||||
|
presented on your old device, and then import the contained key
|
||||||
|
into your keyring.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
Passphrase-Format: numeric9x4
|
||||||
|
Passphrase-Begin: 17
|
||||||
|
|
||||||
|
jA0EBwMCFAxADoCdzeX/0ukBlqI5+pfpKb751qd/7nLNbkpy3gVcaf1QwRPZYt40
|
||||||
|
Ynp08UqRQ2g48ZlnzHLSwlTGOPTuv2Jt8ka+pgZ45xzvJSG2gau03xP4VsC271kR
|
||||||
|
VmCjdb0Y6Rk96mAwfGzrkbaRQ9Z7fIoL866GOv6h9neiVIkp+JYlTV6ISD0ZQJ4Q
|
||||||
|
I6dOQkB/TWZyVjtiJDOQHdfNWliA6NtqaLq19wlu9L5xXjuNpY95KwR8EJXWe0+o
|
||||||
|
Y3d2U/KxOAkXKghP2Qg1GtlPVeGC5T4p03TGI6pzKT+kHX6Rrm9wK6sM9aTquMmF
|
||||||
|
Vok84Jg1DFnwivWC2RILR81rXi7k/+Y6MUbveFgJ9cQduqpxnmD7TjOblYu7M6zp
|
||||||
|
YGAUxh8DRKlIMn2QsA++DBYQ6ACZvwuY8qTDLkqPDo4WqM313dsMJbyGjDdVE7EM
|
||||||
|
PESS+RlABETpZXz8g/ycr6DIUNdlbPcmYlsBfHWDOuR2GFFTwmlv5slWS39dJv38
|
||||||
|
E0eIe1CwdxI801Se7t7dUUS/ZF8wb6GlmxOcqGbF8eko1Z0S64IAm7/h13MRQCxI
|
||||||
|
geQnHfGYVJ2FOimoCMEKwfa9x++RFTDW0u7spDC2uWvK/1viV8OfRppFhLr/kmKb
|
||||||
|
18lWXuAz80DAjUDUsVqEq2MvJBJGoCJUEyjuRsLkHYRM5jYk4v50LyyR0Om73nWF
|
||||||
|
nZBqmqNzdr7Xb9PHHdFhnEc0VvoYbrcM0RVYcEMW3YbmejM891j1d6Iv+/n/qND/
|
||||||
|
NdebGrfWJMmFLf/iEkzTZ3/v5inW9LpWoRc94ioCjJTaEo8Rib6ARRFaJVIsmNXi
|
||||||
|
YicFGO98D+zX+a2t9Yz6IpPajVslnOp6ScpmXgts/2XWD7oE+JgxSAqo/dLVsHgP
|
||||||
|
Ufo=
|
||||||
|
=pulM
|
||||||
|
-----END PGP MESSAGE-----
|
||||||
|
</pre></body></html>
|
||||||
|
--Y6fyGi9SoGeH8WwRaEdC6bbBcYOedDzrQ--
|
||||||
46
chatmaild/src/chatmaild/tests/mail-data/mailer-daemon.eml
Normal file
46
chatmaild/src/chatmaild/tests/mail-data/mailer-daemon.eml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
Date: Fri, 8 Jul 1994 09:21:47 -0400
|
||||||
|
From: Mail Delivery Subsystem <MAILER-DAEMON@example.org>
|
||||||
|
Subject: Returned mail: User unknown
|
||||||
|
To: <owner-ups-mib@CS.UTK.EDU>
|
||||||
|
Auto-Submitted: auto-replied
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/report; report-type=delivery-status;
|
||||||
|
boundary="JAA13167.773673707/CS.UTK.EDU"
|
||||||
|
|
||||||
|
--JAA13167.773673707/CS.UTK.EDU
|
||||||
|
content-type: text/plain; charset=us-ascii
|
||||||
|
|
||||||
|
----- The following addresses had delivery problems -----
|
||||||
|
<arathib@vnet.ibm.com> (unrecoverable error)
|
||||||
|
<wsnell@sdcc13.ucsd.edu> (unrecoverable error)
|
||||||
|
|
||||||
|
--JAA13167.773673707/CS.UTK.EDU
|
||||||
|
content-type: message/delivery-status
|
||||||
|
|
||||||
|
Reporting-MTA: dns; cs.utk.edu
|
||||||
|
|
||||||
|
Original-Recipient: rfc822;arathib@vnet.ibm.com
|
||||||
|
Final-Recipient: rfc822;arathib@vnet.ibm.com
|
||||||
|
Action: failed
|
||||||
|
Status: 5.0.0 (permanent failure)
|
||||||
|
Diagnostic-Code: smtp;
|
||||||
|
550 'arathib@vnet.IBM.COM' is not a registered gateway user
|
||||||
|
Remote-MTA: dns; vnet.ibm.com
|
||||||
|
|
||||||
|
Original-Recipient: rfc822;johnh@hpnjld.njd.hp.com
|
||||||
|
Final-Recipient: rfc822;johnh@hpnjld.njd.hp.com
|
||||||
|
Action: delayed
|
||||||
|
Status: 4.0.0 (hpnjld.njd.jp.com: host name lookup failure)
|
||||||
|
|
||||||
|
Original-Recipient: rfc822;wsnell@sdcc13.ucsd.edu
|
||||||
|
Final-Recipient: rfc822;wsnell@sdcc13.ucsd.edu
|
||||||
|
Action: failed
|
||||||
|
Status: 5.0.0
|
||||||
|
Diagnostic-Code: smtp; 550 user unknown
|
||||||
|
Remote-MTA: dns; sdcc13.ucsd.edu
|
||||||
|
|
||||||
|
--JAA13167.773673707/CS.UTK.EDU
|
||||||
|
content-type: message/rfc822
|
||||||
|
|
||||||
|
[original message goes here]
|
||||||
|
--JAA13167.773673707/CS.UTK.EDU--
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from chatmaild.filtermail import (
|
from chatmaild.filtermail import (
|
||||||
BeforeQueueHandler,
|
IncomingBeforeQueueHandler,
|
||||||
|
OutgoingBeforeQueueHandler,
|
||||||
SendRateLimiter,
|
SendRateLimiter,
|
||||||
check_armored_payload,
|
check_armored_payload,
|
||||||
check_encrypted,
|
check_encrypted,
|
||||||
@@ -18,7 +19,13 @@ def maildomain():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def handler(make_config, maildomain):
|
def handler(make_config, maildomain):
|
||||||
config = make_config(maildomain)
|
config = make_config(maildomain)
|
||||||
return BeforeQueueHandler(config)
|
return OutgoingBeforeQueueHandler(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inhandler(make_config, maildomain):
|
||||||
|
config = make_config(maildomain)
|
||||||
|
return IncomingBeforeQueueHandler(config)
|
||||||
|
|
||||||
|
|
||||||
def test_reject_forged_from(maildata, gencreds, handler):
|
def test_reject_forged_from(maildata, gencreds, handler):
|
||||||
@@ -127,13 +134,27 @@ def test_cleartext_excempt_privacy(maildata, gencreds, handler):
|
|||||||
rcpt_tos = [to_addr, false_to]
|
rcpt_tos = [to_addr, false_to]
|
||||||
content = msg.as_bytes()
|
content = msg.as_bytes()
|
||||||
|
|
||||||
assert "500" in handler.check_DATA(envelope=env2)
|
assert "523" in handler.check_DATA(envelope=env2)
|
||||||
|
|
||||||
|
|
||||||
def test_cleartext_self_send_fails(maildata, gencreds, handler):
|
def test_cleartext_self_send_autocrypt_setup_message(maildata, gencreds, handler):
|
||||||
from_addr = gencreds()[0]
|
from_addr = gencreds()[0]
|
||||||
to_addr = from_addr
|
to_addr = from_addr
|
||||||
|
|
||||||
|
msg = maildata("asm.eml", from_addr=from_addr, to_addr=to_addr)
|
||||||
|
|
||||||
|
class env:
|
||||||
|
mail_from = from_addr
|
||||||
|
rcpt_tos = [to_addr]
|
||||||
|
content = msg.as_bytes()
|
||||||
|
|
||||||
|
assert not handler.check_DATA(envelope=env)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleartext_send_fails(maildata, gencreds, handler):
|
||||||
|
from_addr = gencreds()[0]
|
||||||
|
to_addr = gencreds()[0]
|
||||||
|
|
||||||
msg = maildata("plain.eml", from_addr=from_addr, to_addr=to_addr)
|
msg = maildata("plain.eml", from_addr=from_addr, to_addr=to_addr)
|
||||||
|
|
||||||
class env:
|
class env:
|
||||||
@@ -142,7 +163,41 @@ def test_cleartext_self_send_fails(maildata, gencreds, handler):
|
|||||||
content = msg.as_bytes()
|
content = msg.as_bytes()
|
||||||
|
|
||||||
res = handler.check_DATA(envelope=env)
|
res = handler.check_DATA(envelope=env)
|
||||||
assert "500 Invalid unencrypted" in res
|
assert "523 Encryption Needed" in res
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleartext_incoming_fails(maildata, gencreds, inhandler):
|
||||||
|
from_addr = gencreds()[0]
|
||||||
|
to_addr, password = gencreds()
|
||||||
|
|
||||||
|
msg = maildata("plain.eml", from_addr=from_addr, to_addr=to_addr)
|
||||||
|
|
||||||
|
class env:
|
||||||
|
mail_from = from_addr
|
||||||
|
rcpt_tos = [to_addr]
|
||||||
|
content = msg.as_bytes()
|
||||||
|
|
||||||
|
user = inhandler.config.get_user(to_addr)
|
||||||
|
user.set_password(password)
|
||||||
|
res = inhandler.check_DATA(envelope=env)
|
||||||
|
assert "523 Encryption Needed" in res
|
||||||
|
|
||||||
|
user.allow_incoming_cleartext()
|
||||||
|
assert not inhandler.check_DATA(envelope=env)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleartext_incoming_mailer_daemon(maildata, gencreds, inhandler):
|
||||||
|
from_addr = "mailer-daemon@example.org"
|
||||||
|
to_addr = gencreds()[0]
|
||||||
|
|
||||||
|
msg = maildata("mailer-daemon.eml", from_addr=from_addr, to_addr=to_addr)
|
||||||
|
|
||||||
|
class env:
|
||||||
|
mail_from = from_addr
|
||||||
|
rcpt_tos = [to_addr]
|
||||||
|
content = msg.as_bytes()
|
||||||
|
|
||||||
|
assert not inhandler.check_DATA(envelope=env)
|
||||||
|
|
||||||
|
|
||||||
def test_cleartext_passthrough_domains(maildata, gencreds, handler):
|
def test_cleartext_passthrough_domains(maildata, gencreds, handler):
|
||||||
@@ -166,7 +221,7 @@ def test_cleartext_passthrough_domains(maildata, gencreds, handler):
|
|||||||
rcpt_tos = [to_addr, false_to]
|
rcpt_tos = [to_addr, false_to]
|
||||||
content = msg.as_bytes()
|
content = msg.as_bytes()
|
||||||
|
|
||||||
assert "500" in handler.check_DATA(envelope=env2)
|
assert "523" in handler.check_DATA(envelope=env2)
|
||||||
|
|
||||||
|
|
||||||
def test_cleartext_passthrough_senders(gencreds, handler, maildata):
|
def test_cleartext_passthrough_senders(gencreds, handler, maildata):
|
||||||
|
|||||||
@@ -40,3 +40,17 @@ def test_no_mailboxes_dir(testaddr, example_config, tmp_path):
|
|||||||
user.set_password("someeqkjwelkqwjleqwe")
|
user.set_password("someeqkjwelkqwjleqwe")
|
||||||
user.set_last_login_timestamp(100000)
|
user.set_last_login_timestamp(100000)
|
||||||
assert user.get_last_login_timestamp() == 86400
|
assert user.get_last_login_timestamp() == 86400
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_get_cleartext_flag(testaddr, example_config, tmp_path):
|
||||||
|
p = tmp_path.joinpath("a", "mailboxes")
|
||||||
|
example_config.mailboxes_dir = p
|
||||||
|
|
||||||
|
user = example_config.get_user(testaddr)
|
||||||
|
user.set_password("someeqkjwelkqwjleqwe")
|
||||||
|
user.set_last_login_timestamp(100000)
|
||||||
|
assert user.get_last_login_timestamp() == 86400
|
||||||
|
|
||||||
|
assert not user.is_incoming_cleartext_ok()
|
||||||
|
user.allow_incoming_cleartext()
|
||||||
|
assert user.is_incoming_cleartext_ok()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class User:
|
|||||||
self.maildir = maildir
|
self.maildir = maildir
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
self.password_path = password_path
|
self.password_path = password_path
|
||||||
|
self.enforce_E2EE_path = maildir.joinpath("enforceE2EEincoming")
|
||||||
self.uid = uid
|
self.uid = uid
|
||||||
self.gid = gid
|
self.gid = gid
|
||||||
|
|
||||||
@@ -35,6 +36,13 @@ class User:
|
|||||||
home = str(self.maildir)
|
home = str(self.maildir)
|
||||||
return dict(addr=self.addr, home=home, uid=self.uid, gid=self.gid, password=pw)
|
return dict(addr=self.addr, home=home, uid=self.uid, gid=self.gid, password=pw)
|
||||||
|
|
||||||
|
def is_incoming_cleartext_ok(self):
|
||||||
|
return not self.enforce_E2EE_path.exists()
|
||||||
|
|
||||||
|
def allow_incoming_cleartext(self):
|
||||||
|
if self.enforce_E2EE_path.exists():
|
||||||
|
self.enforce_E2EE_path.unlink()
|
||||||
|
|
||||||
def set_password(self, enc_password):
|
def set_password(self, enc_password):
|
||||||
"""Set the specified password for this user.
|
"""Set the specified password for this user.
|
||||||
|
|
||||||
@@ -50,6 +58,7 @@ class User:
|
|||||||
if not self.addr.startswith("echo@"):
|
if not self.addr.startswith("echo@"):
|
||||||
logging.error(f"could not write password for: {self.addr}")
|
logging.error(f"could not write password for: {self.addr}")
|
||||||
raise
|
raise
|
||||||
|
self.enforce_E2EE_path.touch()
|
||||||
|
|
||||||
def set_last_login_timestamp(self, timestamp):
|
def set_last_login_timestamp(self, timestamp):
|
||||||
"""Track login time with daily granularity
|
"""Track login time with daily granularity
|
||||||
|
|||||||
@@ -106,12 +106,14 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
|||||||
for fn in (
|
for fn in (
|
||||||
"doveauth",
|
"doveauth",
|
||||||
"filtermail",
|
"filtermail",
|
||||||
|
"filtermail-incoming",
|
||||||
"echobot",
|
"echobot",
|
||||||
"chatmail-metadata",
|
"chatmail-metadata",
|
||||||
"lastlogin",
|
"lastlogin",
|
||||||
):
|
):
|
||||||
|
execpath = fn if fn != "filtermail-incoming" else "filtermail"
|
||||||
params = dict(
|
params = dict(
|
||||||
execpath=f"{remote_venv_dir}/bin/{fn}",
|
execpath=f"{remote_venv_dir}/bin/{execpath}",
|
||||||
config_path=remote_chatmail_inipath,
|
config_path=remote_chatmail_inipath,
|
||||||
remote_venv_dir=remote_venv_dir,
|
remote_venv_dir=remote_venv_dir,
|
||||||
mail_domain=config.mail_domain,
|
mail_domain=config.mail_domain,
|
||||||
@@ -541,7 +543,6 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
|
|
||||||
server.group(name="Create vmail group", group="vmail", system=True)
|
server.group(name="Create vmail group", group="vmail", system=True)
|
||||||
server.user(name="Create vmail user", user="vmail", group="vmail", system=True)
|
server.user(name="Create vmail user", user="vmail", group="vmail", system=True)
|
||||||
server.user(name="Create filtermail user", user="filtermail", system=True)
|
|
||||||
server.group(name="Create opendkim group", group="opendkim", system=True)
|
server.group(name="Create opendkim group", group="opendkim", system=True)
|
||||||
server.user(
|
server.user(
|
||||||
name="Create opendkim user",
|
name="Create opendkim user",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ smtp inet n - y - - smtpd -v
|
|||||||
{%- else %}
|
{%- else %}
|
||||||
smtp inet n - y - - smtpd
|
smtp inet n - y - - smtpd
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port_incoming }}
|
||||||
submission inet n - y - 5000 smtpd
|
submission inet n - y - 5000 smtpd
|
||||||
-o syslog_name=postfix/submission
|
-o syslog_name=postfix/submission
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
@@ -76,12 +76,17 @@ anvil unix - - y - 1 anvil
|
|||||||
scache unix - - y - 1 scache
|
scache unix - - y - 1 scache
|
||||||
postlog unix-dgram n - n - 1 postlogd
|
postlog unix-dgram n - n - 1 postlogd
|
||||||
filter unix - n n - - lmtp
|
filter unix - n n - - lmtp
|
||||||
# Local SMTP server for reinjecting filered mail.
|
# Local SMTP server for reinjecting outgoing filtered mail.
|
||||||
localhost:{{ config.postfix_reinject_port }} inet n - n - 10 smtpd
|
localhost:{{ config.postfix_reinject_port }} inet n - n - 10 smtpd
|
||||||
-o syslog_name=postfix/reinject
|
-o syslog_name=postfix/reinject
|
||||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||||
-o cleanup_service_name=authclean
|
-o cleanup_service_name=authclean
|
||||||
|
|
||||||
|
# Local SMTP server for reinjecting incoming filtered mail
|
||||||
|
localhost:{{ config.postfix_reinject_port_incoming }} inet n - n - 10 smtpd
|
||||||
|
-o syslog_name=postfix/reinject_incoming
|
||||||
|
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||||
|
|
||||||
# Cleanup `Received` headers for authenticated mail
|
# Cleanup `Received` headers for authenticated mail
|
||||||
# to avoid leaking client IP.
|
# to avoid leaking client IP.
|
||||||
#
|
#
|
||||||
|
|||||||
12
cmdeploy/src/cmdeploy/service/filtermail-incoming.service.f
Normal file
12
cmdeploy/src/cmdeploy/service/filtermail-incoming.service.f
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Incoming Chatmail Postfix before queue filter
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart={execpath} {config_path} incoming
|
||||||
|
Restart=always
|
||||||
|
RestartSec=30
|
||||||
|
User=vmail
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Chatmail Postfix before queue filter
|
Description=Outgoing Chatmail Postfix before queue filter
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart={execpath} {config_path}
|
ExecStart={execpath} {config_path} outgoing
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
User=filtermail
|
User=vmail
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class TestDC:
|
|||||||
|
|
||||||
def test_ping_pong(self, benchmark, cmfactory):
|
def test_ping_pong(self, benchmark, cmfactory):
|
||||||
ac1, ac2 = cmfactory.get_online_accounts(2)
|
ac1, ac2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
chat = cmfactory.get_protected_chat(ac1, ac2)
|
||||||
|
|
||||||
def dc_ping_pong():
|
def dc_ping_pong():
|
||||||
chat.send_text("ping")
|
chat.send_text("ping")
|
||||||
@@ -49,7 +49,7 @@ class TestDC:
|
|||||||
|
|
||||||
def test_send_10_receive_10(self, benchmark, cmfactory, lp):
|
def test_send_10_receive_10(self, benchmark, cmfactory, lp):
|
||||||
ac1, ac2 = cmfactory.get_online_accounts(2)
|
ac1, ac2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
chat = cmfactory.get_protected_chat(ac1, ac2)
|
||||||
|
|
||||||
def dc_send_10_receive_10():
|
def dc_send_10_receive_10():
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ def test_echobot(cmfactory, chatmail_config, lp, sshdomain):
|
|||||||
ac._evtracker.wait_securejoin_joiner_progress(1000)
|
ac._evtracker.wait_securejoin_joiner_progress(1000)
|
||||||
|
|
||||||
# send message and check it gets replied back
|
# send message and check it gets replied back
|
||||||
lp.sec(f"Send message to echobot")
|
lp.sec("Send message to echobot")
|
||||||
text = "hi, I hope you text me back"
|
text = "hi, I hope you text me back"
|
||||||
chat.send_text(text)
|
chat.send_text(text)
|
||||||
lp.sec("Wait for reply from echobot")
|
lp.sec("Wait for reply from echobot")
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def sshdomain(maildomain):
|
|||||||
def maildomain2():
|
def maildomain2():
|
||||||
domain = os.environ.get("CHATMAIL_DOMAIN2")
|
domain = os.environ.get("CHATMAIL_DOMAIN2")
|
||||||
if not domain:
|
if not domain:
|
||||||
pytest.skip("set CHATMAIL_DOMAIN2 to a ssh-reachable chatmail instance")
|
pytest.skip("set CHATMAIL_DOMAIN2 to a second chatmail server")
|
||||||
return domain
|
return domain
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user