mirror of
https://github.com/chatmail/relay.git
synced 2026-05-13 09:24:43 +00:00
Compare commits
4 Commits
forged_fro
...
basic-post
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9262146e52 | ||
|
|
020ae872f0 | ||
|
|
051fe6ec9f | ||
|
|
75fe5a236a |
@@ -31,9 +31,7 @@ def encrypt_password(password: str):
|
|||||||
|
|
||||||
def create_user(db, user, password):
|
def create_user(db, user, password):
|
||||||
if os.path.exists(NOCREATE_FILE):
|
if os.path.exists(NOCREATE_FILE):
|
||||||
logging.warning(
|
logging.warning(f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation.")
|
||||||
f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
with db.write_transaction() as conn:
|
with db.write_transaction() as conn:
|
||||||
conn.create_user(user, password)
|
conn.create_user(user, password)
|
||||||
@@ -61,7 +59,7 @@ def lookup_passdb(db, user, password):
|
|||||||
return userdata
|
return userdata
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_request(msg, db, mail_domain):
|
def handle_dovecot_request(msg, db):
|
||||||
print(f"received msg: {msg!r}", file=sys.stderr)
|
print(f"received msg: {msg!r}", file=sys.stderr)
|
||||||
short_command = msg[0]
|
short_command = msg[0]
|
||||||
if short_command == "L": # LOOKUP
|
if short_command == "L": # LOOKUP
|
||||||
@@ -72,15 +70,13 @@ def handle_dovecot_request(msg, db, mail_domain):
|
|||||||
res = ""
|
res = ""
|
||||||
if namespace == "shared":
|
if namespace == "shared":
|
||||||
if type == "userdb":
|
if type == "userdb":
|
||||||
if user.endswith(f"@{mail_domain}"):
|
res = lookup_userdb(db, user)
|
||||||
res = lookup_userdb(db, user)
|
|
||||||
if res:
|
if res:
|
||||||
reply_command = "O"
|
reply_command = "O"
|
||||||
else:
|
else:
|
||||||
reply_command = "N"
|
reply_command = "N"
|
||||||
elif type == "passdb":
|
elif type == "passdb":
|
||||||
if user.endswith(f"@{mail_domain}"):
|
res = lookup_passdb(db, user, password=args[0])
|
||||||
res = lookup_passdb(db, user, password=args[0])
|
|
||||||
if res:
|
if res:
|
||||||
reply_command = "O"
|
reply_command = "O"
|
||||||
else:
|
else:
|
||||||
@@ -99,8 +95,6 @@ def main():
|
|||||||
socket = sys.argv[1]
|
socket = sys.argv[1]
|
||||||
passwd_entry = pwd.getpwnam(sys.argv[2])
|
passwd_entry = pwd.getpwnam(sys.argv[2])
|
||||||
db = Database(sys.argv[3])
|
db = Database(sys.argv[3])
|
||||||
with open("/etc/mailname", "r") as fp:
|
|
||||||
mail_domain = fp.read().strip()
|
|
||||||
|
|
||||||
class Handler(StreamRequestHandler):
|
class Handler(StreamRequestHandler):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
@@ -108,7 +102,7 @@ def main():
|
|||||||
msg = self.rfile.readline().strip().decode()
|
msg = self.rfile.readline().strip().decode()
|
||||||
if not msg:
|
if not msg:
|
||||||
break
|
break
|
||||||
res = handle_dovecot_request(msg, db, mail_domain)
|
res = handle_dovecot_request(msg, db)
|
||||||
if res:
|
if res:
|
||||||
print(f"sending result: {res!r}", file=sys.stderr)
|
print(f"sending result: {res!r}", file=sys.stderr)
|
||||||
self.wfile.write(res.encode("ascii"))
|
self.wfile.write(res.encode("ascii"))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from email.parser import BytesParser
|
from email.parser import BytesParser
|
||||||
from email import policy
|
from email import policy
|
||||||
from email.utils import parseaddr
|
|
||||||
|
|
||||||
from aiosmtpd.lmtp import LMTP
|
from aiosmtpd.lmtp import LMTP
|
||||||
from aiosmtpd.controller import UnixSocketController
|
from aiosmtpd.controller import UnixSocketController
|
||||||
@@ -43,14 +42,50 @@ class ExampleHandler:
|
|||||||
return "250 OK"
|
return "250 OK"
|
||||||
|
|
||||||
async def handle_DATA(self, server, session, envelope):
|
async def handle_DATA(self, server, session, envelope):
|
||||||
valid_recipients, res = lmtp_handle_DATA(envelope)
|
logging.info("Processing DATA message from %s", envelope.mail_from)
|
||||||
|
|
||||||
|
valid_recipients = []
|
||||||
|
|
||||||
|
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
|
||||||
|
mail_encrypted = check_encrypted(message)
|
||||||
|
|
||||||
|
res = []
|
||||||
|
for recipient in envelope.rcpt_tos:
|
||||||
|
my_local_domain = envelope.mail_from.split("@")
|
||||||
|
if len(my_local_domain) != 2:
|
||||||
|
res += [f"500 Invalid from address <{envelope.mail_from}>"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if envelope.mail_from == recipient:
|
||||||
|
# Always allow sending emails to self.
|
||||||
|
valid_recipients += [recipient]
|
||||||
|
res += ["250 OK"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
recipient_local_domain = recipient.split("@")
|
||||||
|
if len(recipient_local_domain) != 2:
|
||||||
|
res += [f"500 Invalid address <{recipient}>"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_outgoing = recipient_local_domain[1] != my_local_domain[1]
|
||||||
|
|
||||||
|
if (
|
||||||
|
is_outgoing
|
||||||
|
and not mail_encrypted
|
||||||
|
and message.get("secure-join") != "vc-request"
|
||||||
|
and message.get("secure-join") != "vg-request"
|
||||||
|
):
|
||||||
|
res += ["500 Outgoing mail must be encrypted"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_recipients += [recipient]
|
||||||
|
res += ["250 OK"]
|
||||||
|
|
||||||
# Reinject the mail back into Postfix.
|
# Reinject the mail back into Postfix.
|
||||||
if valid_recipients:
|
if valid_recipients:
|
||||||
logging.info("Reinjecting the mail")
|
logging.info("Reinjecting the mail")
|
||||||
client = SMTPClient("localhost", "10026")
|
client = SMTPClient("localhost", "10026")
|
||||||
client.sendmail(envelope.mail_from, valid_recipients, envelope.content)
|
client.sendmail(envelope.mail_from, valid_recipients, envelope.content)
|
||||||
else:
|
|
||||||
logging.info("no valid recipients, ignoring mail")
|
|
||||||
|
|
||||||
return "\r\n".join(res)
|
return "\r\n".join(res)
|
||||||
|
|
||||||
@@ -62,57 +97,6 @@ async def asyncmain(loop):
|
|||||||
controller.start()
|
controller.start()
|
||||||
|
|
||||||
|
|
||||||
def lmtp_handle_DATA(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)
|
|
||||||
|
|
||||||
valid_recipients = []
|
|
||||||
res = []
|
|
||||||
for recipient in envelope.rcpt_tos:
|
|
||||||
my_local_domain = envelope.mail_from.split("@")
|
|
||||||
if len(my_local_domain) != 2:
|
|
||||||
res += [f"500 Invalid from address <{envelope.mail_from}>"]
|
|
||||||
continue
|
|
||||||
|
|
||||||
_, from_addr = parseaddr(message.get("from").strip())
|
|
||||||
logging.info(f"mime-from: {from_addr} envelope-from: {envelope.mail_from}")
|
|
||||||
if envelope.mail_from.lower() != from_addr.lower():
|
|
||||||
res += [f"500 Invalid FROM <{from_addr}> for <{envelope.mail_from}>"]
|
|
||||||
continue
|
|
||||||
|
|
||||||
if envelope.mail_from == recipient:
|
|
||||||
# Always allow sending emails to self.
|
|
||||||
valid_recipients += [recipient]
|
|
||||||
res += ["250 OK"]
|
|
||||||
continue
|
|
||||||
|
|
||||||
recipient_local_domain = recipient.split("@")
|
|
||||||
if len(recipient_local_domain) != 2:
|
|
||||||
res += [f"500 Invalid address <{recipient}>"]
|
|
||||||
continue
|
|
||||||
|
|
||||||
is_outgoing = recipient_local_domain[1] != my_local_domain[1]
|
|
||||||
|
|
||||||
if (
|
|
||||||
is_outgoing
|
|
||||||
and not mail_encrypted
|
|
||||||
and message.get("secure-join") != "vc-request"
|
|
||||||
and message.get("secure-join") != "vg-request"
|
|
||||||
):
|
|
||||||
res += ["500 Outgoing mail must be encrypted"]
|
|
||||||
continue
|
|
||||||
|
|
||||||
valid_recipients += [recipient]
|
|
||||||
res += ["250 OK"]
|
|
||||||
|
|
||||||
assert len(envelope.rcpt_tos) == len(res)
|
|
||||||
assert len(valid_recipients) <= len(res)
|
|
||||||
return valid_recipients, res
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
|||||||
@@ -1,46 +1,8 @@
|
|||||||
from .filtermail import check_encrypted, lmtp_handle_DATA
|
from .filtermail import check_encrypted
|
||||||
from email.parser import BytesParser
|
from email.parser import BytesParser
|
||||||
from email import policy
|
from email import policy
|
||||||
|
|
||||||
|
|
||||||
def test_reject_forged_from():
|
|
||||||
def makemail(from_addr):
|
|
||||||
return BytesParser(policy=policy.default).parsebytes(
|
|
||||||
"\r\n".join(
|
|
||||||
[
|
|
||||||
f"From: <{from_addr}",
|
|
||||||
"To: <barbaz@c3.testrun.org>",
|
|
||||||
"Date: Sun, 15 Oct 2023 16:41:44 +0000",
|
|
||||||
"Message-ID: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
|
|
||||||
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
|
|
||||||
"Chat-Version: 1.0",
|
|
||||||
"MIME-Version: 1.0",
|
|
||||||
"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no",
|
|
||||||
"",
|
|
||||||
"Hi!",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
).encode()
|
|
||||||
)
|
|
||||||
|
|
||||||
class envelope:
|
|
||||||
mail_from = "bob@c3.testrun.org"
|
|
||||||
rcpt_tos = ["somebody@c3.testrun.org"]
|
|
||||||
|
|
||||||
# test that the filter lets good mail through
|
|
||||||
envelope.content = makemail(envelope.mail_from).as_bytes()
|
|
||||||
valid_recipients, res = lmtp_handle_DATA(envelope=envelope)
|
|
||||||
assert valid_recipients == envelope.rcpt_tos
|
|
||||||
assert len(res) == 1 and "250" in res[0]
|
|
||||||
|
|
||||||
# test that the filter rejects forged mail
|
|
||||||
envelope.content = makemail("forged@c3.testrun.org").as_bytes()
|
|
||||||
valid_recipients, res = lmtp_handle_DATA(envelope=envelope)
|
|
||||||
assert not valid_recipients
|
|
||||||
assert len(res) == 1 and "500" in res[0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_filtermail():
|
def test_filtermail():
|
||||||
def check_encrypted_bstr(content):
|
def check_encrypted_bstr(content):
|
||||||
message = BytesParser(policy=policy.default).parsebytes(content)
|
message = BytesParser(policy=policy.default).parsebytes(content)
|
||||||
|
|||||||
@@ -244,13 +244,6 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
|
|||||||
restarted=dovecot_need_restart,
|
restarted=dovecot_need_restart,
|
||||||
)
|
)
|
||||||
|
|
||||||
# This file is used by auth proxy.
|
|
||||||
# https://wiki.debian.org/EtcMailName
|
|
||||||
server.shell(
|
|
||||||
name="Setup /etc/mailname",
|
|
||||||
commands=[f"echo {mail_domain} >/etc/mailname; chmod 644 /etc/mailname"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def callback():
|
def callback():
|
||||||
result = server.shell(
|
result = server.shell(
|
||||||
commands=[
|
commands=[
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ myhostname = {{ config.domain_name }}
|
|||||||
alias_maps = hash:/etc/aliases
|
alias_maps = hash:/etc/aliases
|
||||||
alias_database = hash:/etc/aliases
|
alias_database = hash:/etc/aliases
|
||||||
|
|
||||||
|
# hard limit, also on internal messages
|
||||||
|
smtpd_client_message_rate_limit = 80
|
||||||
|
|
||||||
# Postfix does not deliver mail for any domain by itself.
|
# Postfix does not deliver mail for any domain by itself.
|
||||||
# Primary domain is listed in `virtual_mailbox_domains` instead
|
# Primary domain is listed in `virtual_mailbox_domains` instead
|
||||||
# and handed over to Dovecot.
|
# and handed over to Dovecot.
|
||||||
|
|||||||
@@ -75,24 +75,6 @@ class ImapConn:
|
|||||||
print(f"imap-login {user!r} {password!r}")
|
print(f"imap-login {user!r} {password!r}")
|
||||||
self.conn.login(user, password)
|
self.conn.login(user, password)
|
||||||
|
|
||||||
def fetch_all(self):
|
|
||||||
print("imap-fetch all")
|
|
||||||
status, res = self.conn.select()
|
|
||||||
if int(res[0]) == 0:
|
|
||||||
raise ValueError("no messages in imap folder")
|
|
||||||
status, results = self.conn.fetch("1:*", "(RFC822)")
|
|
||||||
assert status == "OK"
|
|
||||||
return results
|
|
||||||
|
|
||||||
def fetch_all_messages(self):
|
|
||||||
print("imap-fetch all messages")
|
|
||||||
results = self.fetch_all()
|
|
||||||
messages = []
|
|
||||||
for item in results:
|
|
||||||
if len(item) == 2:
|
|
||||||
messages.append(item[1].decode())
|
|
||||||
return messages
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def smtp(maildomain):
|
def smtp(maildomain):
|
||||||
@@ -115,11 +97,6 @@ class SmtpConn:
|
|||||||
print(f"smtp-login {user!r} {password!r}")
|
print(f"smtp-login {user!r} {password!r}")
|
||||||
self.conn.login(user, password)
|
self.conn.login(user, password)
|
||||||
|
|
||||||
def sendmail(self, from_addr, to_addrs, msg):
|
|
||||||
print(f"smtp-sendmail from={from_addr!r} to_addrs={to_addrs!r}")
|
|
||||||
print(f"smtp-sendmail message size: {len(msg)}")
|
|
||||||
return self.conn.sendmail(from_addr=from_addr, to_addrs=to_addrs, msg=msg)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=["imap", "smtp"])
|
@pytest.fixture(params=["imap", "smtp"])
|
||||||
def imap_or_smtp(request):
|
def imap_or_smtp(request):
|
||||||
@@ -221,66 +198,4 @@ class Remote:
|
|||||||
)
|
)
|
||||||
while 1:
|
while 1:
|
||||||
line = self.popen.stdout.readline()
|
line = self.popen.stdout.readline()
|
||||||
res = line.decode().strip().lower()
|
yield line.decode().strip().lower()
|
||||||
if res:
|
|
||||||
yield res
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mailgen(request):
|
|
||||||
class Mailgen:
|
|
||||||
def get_encrypted(self, from_addr, to_addr):
|
|
||||||
data = request.fspath.dirpath("mailgen/encrypted.eml").read()
|
|
||||||
return data.format(from_addr=from_addr, to_addr=to_addr)
|
|
||||||
|
|
||||||
return Mailgen()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def cmsetup(maildomain, gencreds):
|
|
||||||
return CMSetup(maildomain, gencreds)
|
|
||||||
|
|
||||||
|
|
||||||
class CMSetup:
|
|
||||||
def __init__(self, maildomain, gencreds):
|
|
||||||
self.maildomain = maildomain
|
|
||||||
self.gencreds = gencreds
|
|
||||||
|
|
||||||
def gen_users(self, num):
|
|
||||||
print(f"Creating {num} online users")
|
|
||||||
users = []
|
|
||||||
for i in range(num):
|
|
||||||
addr, password = self.gencreds()
|
|
||||||
user = CMUser(self.maildomain, addr, password)
|
|
||||||
assert user.smtp
|
|
||||||
users.append(user)
|
|
||||||
return users
|
|
||||||
|
|
||||||
|
|
||||||
class CMUser:
|
|
||||||
def __init__(self, maildomain, addr, password):
|
|
||||||
self.maildomain = maildomain
|
|
||||||
self.addr = addr
|
|
||||||
self.password = password
|
|
||||||
self._smtp = None
|
|
||||||
self._imap = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def smtp(self):
|
|
||||||
if not self._smtp:
|
|
||||||
handle = SmtpConn(self.maildomain)
|
|
||||||
handle.connect()
|
|
||||||
handle.login(self.addr, self.password)
|
|
||||||
self._smtp = handle
|
|
||||||
return self._smtp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def imap(self):
|
|
||||||
if not self._imap:
|
|
||||||
imap = ImapConn(self.maildomain)
|
|
||||||
imap.connect()
|
|
||||||
imap.login(self.addr, self.password)
|
|
||||||
self._imap = imap
|
|
||||||
return self._imap
|
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
From: {from_addr}
|
|
||||||
To: {to_addr}
|
|
||||||
Subject: ...
|
|
||||||
Date: Sun, 15 Oct 2023 16:43:21 +0000
|
|
||||||
Message-ID: <Mr.UVyJWZmkCKM.hGzNc6glBE_@c2.testrun.org>
|
|
||||||
In-Reply-To: <Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>
|
|
||||||
References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>
|
|
||||||
<Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Autocrypt: addr={from_addr}; prefer-encrypt=mutual;
|
|
||||||
keydata=xjMEZSwWjhYJKwYBBAHaRw8BAQdAQBEhqeJh0GueHB6kF/DUQqYCxARNBVokg/AzT+7LqH
|
|
||||||
rNFzxiYXJiYXpAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUsFo4CGwMECwkIBwYVCAkKCwID
|
|
||||||
FgIBFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX9A4AEAnHWHp49eBCMHK5t66gYPiW
|
|
||||||
XQuB1mwUjzGfYWB+0RXUoA/0xcQ3FbUNlGKW7Blp6eMFfViv6Mv2d3kNSXACB6nmcMzjgEZSwWjhIK
|
|
||||||
KwYBBAGXVQEFAQEHQBpY5L2M1XHo0uxf8SX1wNLBp/OVvidoWHQF2Jz+kJsUAwEIB8J4BBgWCAAgBQ
|
|
||||||
JlLBaOAhsMFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX/INgEA37AJaNvruYsJVanP
|
|
||||||
IXnYw4CKd55UAwl8Zcy+M2diAbkA/0fHHcGV4r78hpbbL1Os52DPOdqYQRauIeJUeG+G6bQO
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
|
||||||
boundary="YFrteb74qSXmggbOxZL9dRnhymywAi"
|
|
||||||
|
|
||||||
|
|
||||||
--YFrteb74qSXmggbOxZL9dRnhymywAi
|
|
||||||
Content-Description: PGP/MIME version identification
|
|
||||||
Content-Type: application/pgp-encrypted
|
|
||||||
|
|
||||||
Version: 1
|
|
||||||
|
|
||||||
|
|
||||||
--YFrteb74qSXmggbOxZL9dRnhymywAi
|
|
||||||
Content-Description: OpenPGP encrypted message
|
|
||||||
Content-Disposition: inline; filename="encrypted.asc";
|
|
||||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
|
||||||
|
|
||||||
-----BEGIN PGP MESSAGE-----
|
|
||||||
|
|
||||||
wU4DhW3gBZ/VvCYSAQdA8bMs2spwbKdGjVsL1ByPkNrqD7frpB73maeL6I6SzDYg
|
|
||||||
O5G53tv339RdKq3WRcCtEEvxjHlUx2XNwXzC04BpmfvBTgNfPUyLDzjXnxIBB0Ae
|
|
||||||
8ymwGvXMCCimHXN0Dg8Ui62KOi03h0UgheoHWovJSCDF4CKre/xtFr3nL7lq/PKI
|
|
||||||
JsjVNz7/RK9FSXF6WwfONtLCyQGEuVAsB/KXfCBEyfKhaMwGHvhujRidGW5uV1no
|
|
||||||
lMGl3ODmo29Lgeu2uSE7EpJRZoe6hU6ddmBkqxax61ZtkaFlGFFpdo2K8balNNdz
|
|
||||||
ZsJ/9mmI9x3oOJ4/l1nhQbUO9ADbs7gJhFdV5Qkp30b5fCI7bU+aoe1ccBbLe/WM
|
|
||||||
YUty1PqcuQT7XjA+XmYuL261tvW8pBetT+i33/E2d8PzzYt2IuK9qeevyS+yxdwA
|
|
||||||
kfwejFWzzsUlJaDxs1x4XOxkMgSj+jo+g12dFOb7fyClsAnq23iDb8AuaT/BScAI
|
|
||||||
+lO+gher69+6LmM7VGHLG5k762J1jTaQCaKt1s8TAWV99Eo4491vL6fyvk3l/Cfg
|
|
||||||
RXSwiWFgj19Pn0Rq7CD9v22UE2vdUMBTcV4aw79mClk1YQ23jbF0y5DCjPdJ62Zo
|
|
||||||
tskBgFt3NoWV80jZ76zIBLrrjLwCCll8JjJtFwSkt2GX5RFBsVa4A8IDht9RtEk7
|
|
||||||
rrHgbSZQfkauEi/mH3/6CDZoLqSHudUZ7d4MaJwun1TkFYGe2ORwGJd4OBj3oGJp
|
|
||||||
H8YBwCpk///L/fKjX0Gg3M8nrpM4wrRFhPKidAgO/kcm25X4+ZHlVkWBTCt5RWKI
|
|
||||||
fHh6oLDZCqCfcgMkE1KKmwfIHaUkhq5BPRigwy6i5dh1DM4+1UCLh3dxzVbqE9b9
|
|
||||||
61NB19nXdRtDA2sOUnj9ve6m/wEPyCb6/zBQZqvCBYb1/AjdXpUrFT+DbpfyxaXN
|
|
||||||
XfhDVb5mNqNM/IVj0V5fvTc6vOfYbzQtPm10H+FdWWfb+rJRfyC3MA2w2IqstFe3
|
|
||||||
w3bu2iE6CQvSqRvge+ZqLKt/NqYwOURiUmpuklbl3kPJ97+mfKWoiqk8Iz1VY+bb
|
|
||||||
NMUC7aoGv+jcoj+WS6PYO8N6BeRVUUB3ZJSf8nzjgxm1/BcM+UD3BPrlhT11ODRs
|
|
||||||
baifGbprMWwt3dhb8cQgRT8GPdpO1OsDkzL6iikMjLHWWiA99GV6ruiHsIPw6boW
|
|
||||||
A6/uSOskbDHOROotKmddGTBd0iiHXAoQsJFt1ZjUkt6EHrgWs+GAvrvKpXs1mrz8
|
|
||||||
uj3GwEFrHS+Xuf2UDgpszYT3hI2cL/kUtGakVR7m7vVMZqXBUbZdGAEb1PZNPwsI
|
|
||||||
E4aMK02+EVB+tSN4Fzj99N2YD0inVYt+oPjr2tHhUS6aSGBNS/48Ki47DOg4Sxkn
|
|
||||||
lkOWnEbCD+XTnbDd
|
|
||||||
=agR5
|
|
||||||
-----END PGP MESSAGE-----
|
|
||||||
|
|
||||||
|
|
||||||
--YFrteb74qSXmggbOxZL9dRnhymywAi--
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
import smtplib
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def test_remote(remote, imap_or_smtp):
|
def test_remote(remote, imap_or_smtp):
|
||||||
lineproducer = remote.iter_output(imap_or_smtp.logcmd)
|
lineproducer = remote.iter_output(imap_or_smtp.logcmd)
|
||||||
imap_or_smtp.connect()
|
imap_or_smtp.connect()
|
||||||
@@ -17,36 +13,3 @@ def test_use_two_chatmailservers(cmfactory, maildomain2):
|
|||||||
domain1 = ac1.get_config("addr").split("@")[1]
|
domain1 = ac1.get_config("addr").split("@")[1]
|
||||||
domain2 = ac2.get_config("addr").split("@")[1]
|
domain2 = ac2.get_config("addr").split("@")[1]
|
||||||
assert domain1 != domain2
|
assert domain1 != domain2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("forgeaddr", ["internal", "someone@example.org"])
|
|
||||||
def test_reject_forged_from(cmsetup, mailgen, lp, remote, forgeaddr):
|
|
||||||
user1, user3 = cmsetup.gen_users(2)
|
|
||||||
|
|
||||||
lp.sec("send encrypted message with forged from")
|
|
||||||
print("envelope_from", user1.addr)
|
|
||||||
if forgeaddr == "internal":
|
|
||||||
addr_to_forge = cmsetup.gen_users(1)[0].addr
|
|
||||||
else:
|
|
||||||
addr_to_forge = "someone@example.org"
|
|
||||||
|
|
||||||
print("message to inject:")
|
|
||||||
msg = mailgen.get_encrypted(from_addr=addr_to_forge, to_addr=user3.addr)
|
|
||||||
for line in msg.split("\n")[:4]:
|
|
||||||
print(f" {line}")
|
|
||||||
|
|
||||||
lp.sec("Send forged mail and check remote postfix lmtp processing result")
|
|
||||||
remote_log = remote.iter_output("journalctl -t postfix/lmtp")
|
|
||||||
user1.smtp.sendmail(from_addr=user1.addr, to_addrs=[user3.addr], msg=msg)
|
|
||||||
for line in remote_log:
|
|
||||||
# print(line)
|
|
||||||
if "500 invalid from" in line and user3.addr in line:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
pytest.fail("remote postfix/filtermail failed to reject message")
|
|
||||||
|
|
||||||
# check that the logged in user (who sent the forged msg) got a non-delivery notice
|
|
||||||
for message in user1.imap.fetch_all_messages():
|
|
||||||
if "Invalid FROM" in message and addr_to_forge in message:
|
|
||||||
return
|
|
||||||
pytest.fail(f"forged From={addr_to_forge} did not cause non-delivery notice")
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
|
||||||
def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
||||||
@@ -34,3 +35,178 @@ def test_login_same_password(imap_or_smtp, gencreds):
|
|||||||
imap_or_smtp.login(user1, password1)
|
imap_or_smtp.login(user1, password1)
|
||||||
imap_or_smtp.connect()
|
imap_or_smtp.connect()
|
||||||
imap_or_smtp.login(user2, password1)
|
imap_or_smtp.login(user2, password1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="Only rate limit is internal as well now")
|
||||||
|
def test_no_internal_rate_limit(smtp, gencreds):
|
||||||
|
"""Test that there is no rate limit between accounts on the same chatmail server."""
|
||||||
|
user, password = gencreds()
|
||||||
|
to_addr, = gencreds()
|
||||||
|
smtp.connect()
|
||||||
|
smtp.login(user, password)
|
||||||
|
|
||||||
|
mail = "\r\n".join(
|
||||||
|
[
|
||||||
|
"Subject: ...",
|
||||||
|
f"From: <{user}>",
|
||||||
|
f"To: <{to_addr}>",
|
||||||
|
"Date: Sun, 15 Oct 2023 16:43:21 +0000",
|
||||||
|
"Message-ID: <Mr.UVyJWZmkCKM.hGzNc6glBE_@c2.testrun.org>",
|
||||||
|
"In-Reply-To: <Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
|
||||||
|
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
|
||||||
|
"\t<Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
|
||||||
|
"Chat-Version: 1.0",
|
||||||
|
f"Autocrypt: addr={user}; prefer-encrypt=mutual;",
|
||||||
|
"\tkeydata=xjMEZSwWjhYJKwYBBAHaRw8BAQdAQBEhqeJh0GueHB6kF/DUQqYCxARNBVokg/AzT+7LqH",
|
||||||
|
"\trNFzxiYXJiYXpAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUsFo4CGwMECwkIBwYVCAkKCwID",
|
||||||
|
"\tFgIBFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX9A4AEAnHWHp49eBCMHK5t66gYPiW",
|
||||||
|
"\tXQuB1mwUjzGfYWB+0RXUoA/0xcQ3FbUNlGKW7Blp6eMFfViv6Mv2d3kNSXACB6nmcMzjgEZSwWjhIK",
|
||||||
|
"\tKwYBBAGXVQEFAQEHQBpY5L2M1XHo0uxf8SX1wNLBp/OVvidoWHQF2Jz+kJsUAwEIB8J4BBgWCAAgBQ",
|
||||||
|
"\tJlLBaOAhsMFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX/INgEA37AJaNvruYsJVanP",
|
||||||
|
"\tIXnYw4CKd55UAwl8Zcy+M2diAbkA/0fHHcGV4r78hpbbL1Os52DPOdqYQRauIeJUeG+G6bQO",
|
||||||
|
"MIME-Version: 1.0",
|
||||||
|
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";',
|
||||||
|
'\tboundary="YFrteb74qSXmggbOxZL9dRnhymywAi"',
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
|
||||||
|
"Content-Description: PGP/MIME version identification",
|
||||||
|
"Content-Type: application/pgp-encrypted",
|
||||||
|
"",
|
||||||
|
"Version: 1",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
|
||||||
|
"Content-Description: OpenPGP encrypted message",
|
||||||
|
'Content-Disposition: inline; filename="encrypted.asc";',
|
||||||
|
'Content-Type: application/octet-stream; name="encrypted.asc"',
|
||||||
|
"",
|
||||||
|
"-----BEGIN PGP MESSAGE-----",
|
||||||
|
"",
|
||||||
|
"wU4DhW3gBZ/VvCYSAQdA8bMs2spwbKdGjVsL1ByPkNrqD7frpB73maeL6I6SzDYg",
|
||||||
|
"O5G53tv339RdKq3WRcCtEEvxjHlUx2XNwXzC04BpmfvBTgNfPUyLDzjXnxIBB0Ae",
|
||||||
|
"8ymwGvXMCCimHXN0Dg8Ui62KOi03h0UgheoHWovJSCDF4CKre/xtFr3nL7lq/PKI",
|
||||||
|
"JsjVNz7/RK9FSXF6WwfONtLCyQGEuVAsB/KXfCBEyfKhaMwGHvhujRidGW5uV1no",
|
||||||
|
"lMGl3ODmo29Lgeu2uSE7EpJRZoe6hU6ddmBkqxax61ZtkaFlGFFpdo2K8balNNdz",
|
||||||
|
"ZsJ/9mmI9x3oOJ4/l1nhQbUO9ADbs7gJhFdV5Qkp30b5fCI7bU+aoe1ccBbLe/WM",
|
||||||
|
"YUty1PqcuQT7XjA+XmYuL261tvW8pBetT+i33/E2d8PzzYt2IuK9qeevyS+yxdwA",
|
||||||
|
"kfwejFWzzsUlJaDxs1x4XOxkMgSj+jo+g12dFOb7fyClsAnq23iDb8AuaT/BScAI",
|
||||||
|
"+lO+gher69+6LmM7VGHLG5k762J1jTaQCaKt1s8TAWV99Eo4491vL6fyvk3l/Cfg",
|
||||||
|
"RXSwiWFgj19Pn0Rq7CD9v22UE2vdUMBTcV4aw79mClk1YQ23jbF0y5DCjPdJ62Zo",
|
||||||
|
"tskBgFt3NoWV80jZ76zIBLrrjLwCCll8JjJtFwSkt2GX5RFBsVa4A8IDht9RtEk7",
|
||||||
|
"rrHgbSZQfkauEi/mH3/6CDZoLqSHudUZ7d4MaJwun1TkFYGe2ORwGJd4OBj3oGJp",
|
||||||
|
"H8YBwCpk///L/fKjX0Gg3M8nrpM4wrRFhPKidAgO/kcm25X4+ZHlVkWBTCt5RWKI",
|
||||||
|
"fHh6oLDZCqCfcgMkE1KKmwfIHaUkhq5BPRigwy6i5dh1DM4+1UCLh3dxzVbqE9b9",
|
||||||
|
"61NB19nXdRtDA2sOUnj9ve6m/wEPyCb6/zBQZqvCBYb1/AjdXpUrFT+DbpfyxaXN",
|
||||||
|
"XfhDVb5mNqNM/IVj0V5fvTc6vOfYbzQtPm10H+FdWWfb+rJRfyC3MA2w2IqstFe3",
|
||||||
|
"w3bu2iE6CQvSqRvge+ZqLKt/NqYwOURiUmpuklbl3kPJ97+mfKWoiqk8Iz1VY+bb",
|
||||||
|
"NMUC7aoGv+jcoj+WS6PYO8N6BeRVUUB3ZJSf8nzjgxm1/BcM+UD3BPrlhT11ODRs",
|
||||||
|
"baifGbprMWwt3dhb8cQgRT8GPdpO1OsDkzL6iikMjLHWWiA99GV6ruiHsIPw6boW",
|
||||||
|
"A6/uSOskbDHOROotKmddGTBd0iiHXAoQsJFt1ZjUkt6EHrgWs+GAvrvKpXs1mrz8",
|
||||||
|
"uj3GwEFrHS+Xuf2UDgpszYT3hI2cL/kUtGakVR7m7vVMZqXBUbZdGAEb1PZNPwsI",
|
||||||
|
"E4aMK02+EVB+tSN4Fzj99N2YD0inVYt+oPjr2tHhUS6aSGBNS/48Ki47DOg4Sxkn",
|
||||||
|
"lkOWnEbCD+XTnbDd",
|
||||||
|
"=agR5",
|
||||||
|
"-----END PGP MESSAGE-----",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"--YFrteb74qSXmggbOxZL9dRnhymywAi--",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
).encode()
|
||||||
|
for i in range(100):
|
||||||
|
print("Sending mail", str(i))
|
||||||
|
smtp.conn.sendmail(user, to_addr, mail)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exceed_rate_limit(smtp, gencreds):
|
||||||
|
"""Test that the outbound rate limit is exceeded if we send a lot of messages at once."""
|
||||||
|
user, password = gencreds()
|
||||||
|
smtp.connect()
|
||||||
|
smtp.login(user, password)
|
||||||
|
|
||||||
|
to_addr = "foobar@example.org"
|
||||||
|
mail = "\r\n".join(
|
||||||
|
[
|
||||||
|
"Subject: ...",
|
||||||
|
f"From: <{user}>",
|
||||||
|
f"To: <{to_addr}>",
|
||||||
|
"Date: Sun, 15 Oct 2023 16:43:21 +0000",
|
||||||
|
"Message-ID: <Mr.UVyJWZmkCKM.hGzNc6glBE_@c2.testrun.org>",
|
||||||
|
"In-Reply-To: <Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
|
||||||
|
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
|
||||||
|
"\t<Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
|
||||||
|
"Chat-Version: 1.0",
|
||||||
|
f"Autocrypt: addr={user}; prefer-encrypt=mutual;",
|
||||||
|
"\tkeydata=xjMEZSwWjhYJKwYBBAHaRw8BAQdAQBEhqeJh0GueHB6kF/DUQqYCxARNBVokg/AzT+7LqH",
|
||||||
|
"\trNFzxiYXJiYXpAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUsFo4CGwMECwkIBwYVCAkKCwID",
|
||||||
|
"\tFgIBFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX9A4AEAnHWHp49eBCMHK5t66gYPiW",
|
||||||
|
"\tXQuB1mwUjzGfYWB+0RXUoA/0xcQ3FbUNlGKW7Blp6eMFfViv6Mv2d3kNSXACB6nmcMzjgEZSwWjhIK",
|
||||||
|
"\tKwYBBAGXVQEFAQEHQBpY5L2M1XHo0uxf8SX1wNLBp/OVvidoWHQF2Jz+kJsUAwEIB8J4BBgWCAAgBQ",
|
||||||
|
"\tJlLBaOAhsMFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX/INgEA37AJaNvruYsJVanP",
|
||||||
|
"\tIXnYw4CKd55UAwl8Zcy+M2diAbkA/0fHHcGV4r78hpbbL1Os52DPOdqYQRauIeJUeG+G6bQO",
|
||||||
|
"MIME-Version: 1.0",
|
||||||
|
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";',
|
||||||
|
'\tboundary="YFrteb74qSXmggbOxZL9dRnhymywAi"',
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
|
||||||
|
"Content-Description: PGP/MIME version identification",
|
||||||
|
"Content-Type: application/pgp-encrypted",
|
||||||
|
"",
|
||||||
|
"Version: 1",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
|
||||||
|
"Content-Description: OpenPGP encrypted message",
|
||||||
|
'Content-Disposition: inline; filename="encrypted.asc";',
|
||||||
|
'Content-Type: application/octet-stream; name="encrypted.asc"',
|
||||||
|
"",
|
||||||
|
"-----BEGIN PGP MESSAGE-----",
|
||||||
|
"",
|
||||||
|
"wU4DhW3gBZ/VvCYSAQdA8bMs2spwbKdGjVsL1ByPkNrqD7frpB73maeL6I6SzDYg",
|
||||||
|
"O5G53tv339RdKq3WRcCtEEvxjHlUx2XNwXzC04BpmfvBTgNfPUyLDzjXnxIBB0Ae",
|
||||||
|
"8ymwGvXMCCimHXN0Dg8Ui62KOi03h0UgheoHWovJSCDF4CKre/xtFr3nL7lq/PKI",
|
||||||
|
"JsjVNz7/RK9FSXF6WwfONtLCyQGEuVAsB/KXfCBEyfKhaMwGHvhujRidGW5uV1no",
|
||||||
|
"lMGl3ODmo29Lgeu2uSE7EpJRZoe6hU6ddmBkqxax61ZtkaFlGFFpdo2K8balNNdz",
|
||||||
|
"ZsJ/9mmI9x3oOJ4/l1nhQbUO9ADbs7gJhFdV5Qkp30b5fCI7bU+aoe1ccBbLe/WM",
|
||||||
|
"YUty1PqcuQT7XjA+XmYuL261tvW8pBetT+i33/E2d8PzzYt2IuK9qeevyS+yxdwA",
|
||||||
|
"kfwejFWzzsUlJaDxs1x4XOxkMgSj+jo+g12dFOb7fyClsAnq23iDb8AuaT/BScAI",
|
||||||
|
"+lO+gher69+6LmM7VGHLG5k762J1jTaQCaKt1s8TAWV99Eo4491vL6fyvk3l/Cfg",
|
||||||
|
"RXSwiWFgj19Pn0Rq7CD9v22UE2vdUMBTcV4aw79mClk1YQ23jbF0y5DCjPdJ62Zo",
|
||||||
|
"tskBgFt3NoWV80jZ76zIBLrrjLwCCll8JjJtFwSkt2GX5RFBsVa4A8IDht9RtEk7",
|
||||||
|
"rrHgbSZQfkauEi/mH3/6CDZoLqSHudUZ7d4MaJwun1TkFYGe2ORwGJd4OBj3oGJp",
|
||||||
|
"H8YBwCpk///L/fKjX0Gg3M8nrpM4wrRFhPKidAgO/kcm25X4+ZHlVkWBTCt5RWKI",
|
||||||
|
"fHh6oLDZCqCfcgMkE1KKmwfIHaUkhq5BPRigwy6i5dh1DM4+1UCLh3dxzVbqE9b9",
|
||||||
|
"61NB19nXdRtDA2sOUnj9ve6m/wEPyCb6/zBQZqvCBYb1/AjdXpUrFT+DbpfyxaXN",
|
||||||
|
"XfhDVb5mNqNM/IVj0V5fvTc6vOfYbzQtPm10H+FdWWfb+rJRfyC3MA2w2IqstFe3",
|
||||||
|
"w3bu2iE6CQvSqRvge+ZqLKt/NqYwOURiUmpuklbl3kPJ97+mfKWoiqk8Iz1VY+bb",
|
||||||
|
"NMUC7aoGv+jcoj+WS6PYO8N6BeRVUUB3ZJSf8nzjgxm1/BcM+UD3BPrlhT11ODRs",
|
||||||
|
"baifGbprMWwt3dhb8cQgRT8GPdpO1OsDkzL6iikMjLHWWiA99GV6ruiHsIPw6boW",
|
||||||
|
"A6/uSOskbDHOROotKmddGTBd0iiHXAoQsJFt1ZjUkt6EHrgWs+GAvrvKpXs1mrz8",
|
||||||
|
"uj3GwEFrHS+Xuf2UDgpszYT3hI2cL/kUtGakVR7m7vVMZqXBUbZdGAEb1PZNPwsI",
|
||||||
|
"E4aMK02+EVB+tSN4Fzj99N2YD0inVYt+oPjr2tHhUS6aSGBNS/48Ki47DOg4Sxkn",
|
||||||
|
"lkOWnEbCD+XTnbDd",
|
||||||
|
"=agR5",
|
||||||
|
"-----END PGP MESSAGE-----",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"--YFrteb74qSXmggbOxZL9dRnhymywAi--",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
).encode()
|
||||||
|
for i in range(100):
|
||||||
|
print("Sending mail", str(i))
|
||||||
|
try:
|
||||||
|
smtp.conn.sendmail(user, to_addr, mail)
|
||||||
|
except smtplib.SMTPSenderRefused as e:
|
||||||
|
if i == 0:
|
||||||
|
pytest.fail(f"rate limit was exceeded too early with msg {i} - maybe wait a minute before testing?")
|
||||||
|
if i < 41:
|
||||||
|
pytest.fail(f"rate limit was exceeded too early with msg {i}")
|
||||||
|
assert e.smtp_code == 450
|
||||||
|
assert b'4.7.1 Error: too much mail from' in e.smtp_error
|
||||||
|
return
|
||||||
|
pytest.fail("Rate limit was not exceeded")
|
||||||
|
|||||||
Reference in New Issue
Block a user