mirror of
https://github.com/chatmail/relay.git
synced 2026-05-13 17:34:38 +00:00
Compare commits
7 Commits
basic-post
...
forged_fro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
801b035f18 | ||
|
|
6f8823cbeb | ||
|
|
c0673cc43f | ||
|
|
6dcd686701 | ||
|
|
087ae78edc | ||
|
|
7e93299bce | ||
|
|
1ea25eb28c |
@@ -31,7 +31,9 @@ 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(f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation.")
|
logging.warning(
|
||||||
|
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)
|
||||||
@@ -59,7 +61,7 @@ def lookup_passdb(db, user, password):
|
|||||||
return userdata
|
return userdata
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_request(msg, db):
|
def handle_dovecot_request(msg, db, mail_domain):
|
||||||
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
|
||||||
@@ -70,13 +72,15 @@ def handle_dovecot_request(msg, db):
|
|||||||
res = ""
|
res = ""
|
||||||
if namespace == "shared":
|
if namespace == "shared":
|
||||||
if type == "userdb":
|
if type == "userdb":
|
||||||
res = lookup_userdb(db, user)
|
if user.endswith(f"@{mail_domain}"):
|
||||||
|
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":
|
||||||
res = lookup_passdb(db, user, password=args[0])
|
if user.endswith(f"@{mail_domain}"):
|
||||||
|
res = lookup_passdb(db, user, password=args[0])
|
||||||
if res:
|
if res:
|
||||||
reply_command = "O"
|
reply_command = "O"
|
||||||
else:
|
else:
|
||||||
@@ -95,6 +99,8 @@ 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):
|
||||||
@@ -102,7 +108,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)
|
res = handle_dovecot_request(msg, db, mail_domain)
|
||||||
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,6 +3,7 @@ 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
|
||||||
@@ -42,50 +43,14 @@ class ExampleHandler:
|
|||||||
return "250 OK"
|
return "250 OK"
|
||||||
|
|
||||||
async def handle_DATA(self, server, session, envelope):
|
async def handle_DATA(self, server, session, envelope):
|
||||||
logging.info("Processing DATA message from %s", envelope.mail_from)
|
valid_recipients, res = lmtp_handle_DATA(envelope)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -97,6 +62,57 @@ 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,8 +1,46 @@
|
|||||||
from .filtermail import check_encrypted
|
from .filtermail import check_encrypted, lmtp_handle_DATA
|
||||||
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,6 +244,13 @@ 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,9 +29,6 @@ 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,6 +75,24 @@ 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):
|
||||||
@@ -97,6 +115,11 @@ 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):
|
||||||
@@ -198,4 +221,66 @@ class Remote:
|
|||||||
)
|
)
|
||||||
while 1:
|
while 1:
|
||||||
line = self.popen.stdout.readline()
|
line = self.popen.stdout.readline()
|
||||||
yield line.decode().strip().lower()
|
res = 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
|
||||||
|
|||||||
66
online-tests/mailgen/encrypted.eml
Normal file
66
online-tests/mailgen/encrypted.eml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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,3 +1,7 @@
|
|||||||
|
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()
|
||||||
@@ -13,3 +17,36 @@ 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,5 +1,4 @@
|
|||||||
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):
|
||||||
@@ -35,178 +34,3 @@ 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