This commit is contained in:
Shelvacu
2025-04-09 21:37:24 -07:00
committed by Shelvacu on fw
parent c1c5f39a00
commit cfa5049922
7 changed files with 274 additions and 4 deletions

View File

@@ -463,7 +463,8 @@
inherit self;
inherit (inputs) nixpkgs;
});
"dns/python-env" = dns.passthru.python;
"dns/python-env" = builtins.dirOf (builtins.dirOf dns.interpreter);
"mailtest/python-env" = builtins.dirOf (builtins.dirOf self.checks.x86_64-linux.liam.nodes.checker.vacu.mailtest.smtp.interpreter);
};
haproxy-auth-request = pkgs.callPackage ./packages/haproxy-auth-request.nix {
inherit haproxy-lua-http;

View File

@@ -15,3 +15,7 @@ extraPaths = [
[[tool.pyright.executionEnvironments]]
root = "scripts/dns"
extraPaths = [ ".generated/dns/python-env/lib/python3.12/site-packages" ]
[[tool.pyright.executionEnvironments]]
root = "tests/liam/mailtest"
extraPaths = [ ".generated/mailtest/python-env/lib/python3.12/site-packages" ]

View File

@@ -69,6 +69,31 @@ let
mkdir -p $out/liam
SOPS_AGE_KEY="${testAgeSecret}" ${pkgs.sops}/bin/sops --verbose -e --age "$(echo "${testAgeSecret}" | ${pkgs.age}/bin/age-keygen -y)" ${sopsTestSecretsYaml} --output-type yaml > $out/liam/main.yaml
'';
mailtestModule = {
config,
pkgs,
lib,
...
}:
let
libraries = with pkgs.python3Packages; [
imap-tools
requests
];
mkPkg = name: pkgs.writers.writePython3Bin "mailtest-${name}" { inherit libraries; } ''
# flake8: noqa
${builtins.readFile ./mailtest/${name}.py}
'';
in
{
options.vacu.mailtest = lib.mkOption { readOnly = true; };
config.vacu.mailtest = {
smtp = mkPkg "smtp";
imap = mkPkg "imap";
mailpit = mkPkg "mailpit";
};
config.environment.systemPackages = [ config.vacu.mailtest.smtp config.vacu.mailtest.imap ];
};
in
{
name = "liam-receives-mail";
@@ -185,10 +210,9 @@ in
nodes.checker =
{ pkgs, lib, ... }:
{
imports = [ mailtestModule ];
environment.systemPackages = [
pkgs.wget
pkgs.python311Packages.imap-tools
pkgs.python311
(pkgs.writers.writePython3Bin "mailtest"
{
libraries = with pkgs.python3Packages; [

120
tests/liam/mailtest/imap.py Normal file
View File

@@ -0,0 +1,120 @@
import imaplib
import imap_tools
import argparse
import json
from .util import *
from typing import NamedTuple
parser = argparse.ArgumentParser()
parser.add_argument("host", type=str)
parser.add_argument("--imap-insecure", default=False, action="store_true")
parser.add_argument("--imap-move-to")
parser.add_argument("--imap-dir", default=None)
parser.add_argument("--username")
parser.add_argument("--password")
parser.add_argument("--message-magic", type=str)
args = parser.parse_args()
msg_magic = args.message_magic
info(f"got args {args!r}")
username = args.username
password = args.password
if password is None:
password = username
MessageInFolder = NamedTuple(
"MessageInFolder", [("message", imap_tools.message.MailMessage), ("folder", str)]
)
info(f"looking for {msg_magic}")
result = ""
matching_messages = []
try:
def connection() -> imap_tools.BaseMailBox:
return imap_tools.MailBox(args.host, ssl_context=mk_ctx()).login(
username, password
)
def find_messages(mailbox: imap_tools.BaseMailBox) -> list[MessageInFolder]:
matching_messages = []
directories = []
for d in mailbox.folder.list():
if "\\Noselect" not in d.flags:
directories.append(d.name)
# info(f"directories is {directories!r}")
for imap_dir in directories:
info(f"checking in {imap_dir!r}")
mailbox.folder.set(imap_dir)
# except imap_tools.errors.MailboxFolderSelectError as e:
# # info(f"failed to select folder {e!r}")
# continue
for msg in mailbox.fetch(mark_seen=False):
if "\\Deleted" in msg.flags:
continue
# info(f"found message {msg.uid!r} with text {msg.text!r} in folder {imap_dir!r}")
msg_str = msg.obj.as_string()
info(f"flags: {msg.flags!r}")
info(f"{msg_str}")
if msg_magic == msg.text.strip():
in_folder = MessageInFolder(message=msg, folder=imap_dir)
matching_messages.append(in_folder)
return matching_messages
if args.imap_move_to is not None:
with connection() as mailbox:
info("prefind")
prefind = find_messages(mailbox)
assert len(prefind) > 0, "Could not find message to move anywhere"
assert len(prefind) == 1, "Found duplicate messages"
mailbox.folder.set(prefind[0].folder)
msg = prefind[0].message
uid = msg.uid
assert uid is not None
info(f"about to move {uid} to {args.imap_move_to}")
res = mailbox.move(uid, args.imap_move_to)
assert res[1][1][1] is not None, "failed to move" # type: ignore
info(f"done moving, res {res!r}")
with connection() as mailbox:
matching_messages = find_messages(mailbox)
if args.expect == "received":
# print(f"{matching_messages!r}")
assert (
len(matching_messages) > 0
), "Could not find the message in the mailbox"
assert (
len(matching_messages) == 1
), f"Multiple messages matching message magic {msg_magic}"
matching_mif = matching_messages[0]
if args.imap_dir is not None:
expected_dir = args.imap_dir
actual_dir = matching_mif.folder
assert (
expected_dir == actual_dir
), f"Expected to find message in {expected_dir}, found it in {actual_dir} instead"
matching_message = matching_mif.message
for expected_flag in args.expect_flag:
assert (
expected_flag in matching_message.flags
), f"Flag {expected_flag} not found, message flags: {matching_message.flags!r}"
except imaplib.IMAP4.error as e:
info(f"IMAP error {e!r}")
result = "error"
assert args.expect == "imap_error", f"IMAP error: {e}"
else:
result = "success"
def mail_to_jsonish(m: MessageInFolder) -> dict:
return {
"folder": m.folder,
"flags": m.message.flags,
"body": m.message.text.strip(),
}
print_json(
result = result,
messages = [mail_to_jsonish(m) for m in matching_messages],
)

View File

@@ -0,0 +1,54 @@
import smtplib
import argparse
import json
from .util import *
parser = argparse.ArgumentParser()
parser.add_argument("host", type=str)
parser.add_argument("--mailfrom", default="foo@example.com")
parser.add_argument("--rcptto", default="awesome@vacu.store")
parser.add_argument("--subject", default="Some test message")
parser.add_argument("--header", action="append", default=[])
parser.add_argument("--submission", default=False, action="store_true")
parser.add_argument("--smtp-starttls", default=None, action="store_true")
parser.add_argument("--username")
parser.add_argument("--password")
parser.add_argument("--message-magic", type=str)
args = parser.parse_args()
info(f"got args {args!r}")
args.header.append(f"Subject: {args.subject}")
username = args.username
password = args.password
if password is None:
password = username
result = ""
try:
smtp = None
if args.submission:
smtp = smtplib.SMTP_SSL(args.host, port=465, context=mk_ctx())
else:
smtp = smtplib.SMTP(args.host, port=25)
smtp.ehlo()
if args.smtp_starttls:
smtp.starttls(context=mk_ctx())
smtp.ehlo()
if args.submission:
smtp.login(username, password)
headers = "\n".join(args.header)
smtp.sendmail(args.mailfrom, args.rcptto, f"{headers}\n\n{args.message_magic}")
smtp.close()
except smtplib.SMTPRecipientsRefused:
result = False
except smtplib.SMTPSenderRefused:
result = False
else:
result = True
print_json(result = result)

View File

@@ -0,0 +1,15 @@
import sys
import ssl
import json
def info(msg: str):
print(msg, file=sys.stderr)
def mk_ctx():
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def print_json(**kwargs):
print(json.dumps(kwargs))

View File

@@ -1,9 +1,12 @@
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from hints import *
import json
import shlex
import uuid
DATA_JSON = """
@data@
"""
@@ -25,6 +28,55 @@ liam.wait_for_unit("postfix.service")
liam.wait_for_unit("dovecot2.service")
relay.wait_for_unit("mailpit.service")
def make_command(args: list) -> str:
return " ".join(map(shlex.quote, (map(str, args))))
class TesterThing():
uuid: str = ""
default_smtp: dict[str, str] = {}
default_imap: dict[str, str] = {}
default_mailpit: dict[str, str] = {}
def __init__(self, username: str, smtp: dict[str, str] = {}, imap: dict[str, str] = {}, mailpit: dict[str, str] = {}):
self.uuid = str(uuid.uuid4())
self.default_smtp = {
"rcptto": "someone@example.com",
"username": username,
**smtp
}
self.default_imap = {
"username": username,
**imap
}
self.default_mailpit = {
"mailpit-url": f"http://{relay_ip}:8025",
**mailpit
}
def run_expecting_json(self, name: str, **kwargs: dict[str, str]) -> dict[str, Any]:
args:list[str] = [name]
for k, v in kwargs:
dashed = k.replace("_","-")
args.append(f"--{dashed}")
args.append(v)
res = checker.succeed(make_command(args))
res = res.strip()
assert res != ""
return json.loads(res)
def run_smtp(self, **kwargs: dict[str, str]) -> bool:
args = {"message_magic": self.uuid, **self.default_smtp, **kwargs}
res = self.run_expecting_json("mailpit-smtp", **args)
return res["result"]
def smtp_accepted(self, **kwargs):
res = self.run_smtp(**kwargs)
assert res, "Message was not accepted when it should have been"
def smtp_rejected(self, **kwargs):
res = self.run_smtp(**kwargs)
assert not res, "Message was accepted when it was supposed to be rejected"
# The order of these shouldn't matter, other than what fails first. Whatever is at the top is probably whatever I was working on most recently.
checks = f"""
--submission --mailfrom robot@vacu.store --rcptto someone@example.com --username vacustore --expect-mailpit-received --mailpit-url http://{relay_ip}:8025