wip
This commit is contained in:
@@ -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;
|
||||
|
@@ -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" ]
|
||||
|
@@ -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
120
tests/liam/mailtest/imap.py
Normal 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],
|
||||
)
|
54
tests/liam/mailtest/smtp.py
Normal file
54
tests/liam/mailtest/smtp.py
Normal 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)
|
15
tests/liam/mailtest/util.py
Normal file
15
tests/liam/mailtest/util.py
Normal 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))
|
@@ -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
|
||||
|
Reference in New Issue
Block a user