From b2f1efc73dfb79b851f8e683d6ed94df7811a9cc Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Mon, 27 Jul 2026 13:05:00 +0200 Subject: [PATCH] Test SMTP size and disk-space boundaries --- contrib/testing/README.md | 21 + contrib/testing/smtp-size-check.py | 487 ++++++++++++++++++++++++ contrib/testing/smtp-size-disk-check.sh | 105 +++++ docs/release-testing-0.7.md | 2 +- docs/test-evidence/0.7-r1.md | 42 ++ 5 files changed, 656 insertions(+), 1 deletion(-) create mode 100755 contrib/testing/smtp-size-check.py create mode 100755 contrib/testing/smtp-size-disk-check.sh diff --git a/contrib/testing/README.md b/contrib/testing/README.md index 0af72e0..ba529ff 100644 --- a/contrib/testing/README.md +++ b/contrib/testing/README.md @@ -23,6 +23,13 @@ one-off files in `/tmp`: domain. It also covers a forged local envelope sender, the null reverse path, and historical source-route, percent-hack, and bang-path recipient forms without committing a message. +- `smtp-size-disk-check.sh` runs the `smtp-size-check.py` boundary probe with + a temporary 1024-byte message limit, verifies the advertised `SIZE`, exact + acceptance, declared and undeclared over-limit rejection, Store delivery, + and cleanup, then mounts the same isolated 8-MiB full-spool fixture used by + the Queue tests. Both a predeclared size and a `MAIL FROM` without `SIZE` + must receive `452 4.3.1`. Its trap restores the original SMTP document, + spool mount, service, Queue, and Store state. - `smtp-submission-auth-check.py` exercises port 587 STARTTLS and port 465 implicit TLS directly, including the shared GSASL PLAIN and LOGIN mechanisms, pre-TLS AUTH rejection, invalid credentials, and repeated @@ -296,6 +303,20 @@ It requires passwordless permission for Bongo service start/stop/restart, `mount`, `umount`, and `stat` on the dedicated test spool. Run it only on a disposable test installation. +The SMTP size and disk-space check uses the same mount permissions and also +requires the retained disposable local account: + +```sh +export BONGO_ALLOW_LIVE_SMTP_TEST=1 +export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters' +./contrib/testing/smtp-size-disk-check.sh +``` + +It refuses to hide a non-empty Queue spool. The successful exact-limit +message and any uniquely marked leftovers from an interrupted run are removed +from Queue and Store before the original unlimited/default size setting is +restored. + The Store protocol safety check is non-destructive and requires the two disposable users retained by `store-user-layout-check.sh`: diff --git a/contrib/testing/smtp-size-check.py b/contrib/testing/smtp-size-check.py new file mode 100755 index 0000000..1add92f --- /dev/null +++ b/contrib/testing/smtp-size-check.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 +# This program is free software, licensed under the terms of the GNU GPL. +# See the Bongo COPYING file for full details. +# Copyright (c) 2026 Bongo Project contributors + +"""Exercise SMTP SIZE boundaries and low Queue-space responses.""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +import re +import socket +import subprocess +import sys +import time + +from bongo.store.StoreClient import StoreClient + + +ADMIN = os.environ.get("BONGO_TEST_ADMIN", "/usr/bin/bongo-admin") +QUEUE_TOOL = os.environ.get( + "BONGO_TEST_QUEUE_TOOL", "/usr/bin/bongo-queuetool" +) +SYSTEMCTL = os.environ.get("BONGO_TEST_SYSTEMCTL", "/usr/bin/systemctl") +HOST = os.environ.get("BONGO_TEST_HOST", "127.0.0.1") +PORT = int(os.environ.get("BONGO_TEST_SMTP_PORT", "25")) +STORE_PORT = int(os.environ.get("BONGO_TEST_STORE_PORT", "689")) +USER = os.environ.get("BONGO_TEST_USER", "test1") +DOMAIN = os.environ.get("BONGO_TEST_DOMAIN", "bongo.test") +PASSWORD = os.environ.get("BONGO_TEST_PASSWORD", "") +LIMIT = int(os.environ.get("BONGO_TEST_MESSAGE_LIMIT", "1024")) +TIMEOUT = float(os.environ.get("BONGO_TEST_TIMEOUT", "30")) +STARTUP_GRACE = float(os.environ.get("BONGO_TEST_STARTUP_GRACE", "2")) +ALLOW_LIVE = os.environ.get("BONGO_ALLOW_LIVE_SMTP_TEST") == "1" +TOKEN = f"smtp11-{os.getpid()}-{int(time.time())}" +TOKEN_PREFIX = b"smtp11-" +RESPONSE = re.compile(rb"^([0-9]{3})([- ])(.*)\r\n$") +QUEUE_ID = re.compile(r"^[0-9A-Fa-f]{3}-[0-9A-Fa-f]+$") + + +class SMTP11Error(RuntimeError): + """Raised when SMTP size or free-space handling is incorrect.""" + + +def run( + arguments: list[str], + *, + input_text: str | None = None, + binary: bool = False, + check: bool = True, +) -> subprocess.CompletedProcess: + completed = subprocess.run( + arguments, + input=input_text, + capture_output=True, + text=not binary, + check=False, + ) + if check and completed.returncode: + stdout = completed.stdout + stderr = completed.stderr + if binary: + stdout = stdout.decode("utf-8", "replace") + stderr = stderr.decode("utf-8", "replace") + raise SMTP11Error( + f"{' '.join(arguments)} failed: " + f"{stderr.strip() or stdout.strip()}" + ) + return completed + + +def admin(*arguments: str, input_text: str | None = None) -> str: + return run( + ["sudo", "-n", ADMIN, *arguments], input_text=input_text + ).stdout + + +def queue( + *arguments: str, binary: bool = False, check: bool = True +) -> subprocess.CompletedProcess: + return run( + ["sudo", "-n", "-u", "bongo", QUEUE_TOOL, *arguments], + binary=binary, + check=check, + ) + + +def read_configuration() -> dict: + try: + value = json.loads(admin("__config-read", "smtp")) + except json.JSONDecodeError as error: + raise SMTP11Error( + "bongo-admin returned invalid SMTP configuration" + ) from error + if not isinstance(value, dict): + raise SMTP11Error("SMTP configuration is not an object") + return value + + +def replace_configuration(configuration: dict) -> None: + admin( + "__config-replace", + "smtp", + input_text=json.dumps(configuration, separators=(",", ":")), + ) + + +class SMTPConnection: + def __init__(self) -> None: + self.socket = socket.create_connection((HOST, PORT), timeout=TIMEOUT) + self.socket.settimeout(TIMEOUT) + self.reader = self.socket.makefile("rb") + + def close(self) -> None: + try: + self.reader.close() + finally: + self.socket.close() + + def response(self) -> tuple[int, list[bytes]]: + lines: list[bytes] = [] + response_code: int | None = None + while True: + line = self.reader.readline(8194) + if not line: + raise SMTP11Error("connection closed while awaiting response") + if len(line) > 8192: + raise SMTP11Error("SMTP response line exceeded 8192 bytes") + match = RESPONSE.match(line) + if match is None: + raise SMTP11Error(f"malformed SMTP response: {line!r}") + code = int(match.group(1)) + if response_code is None: + response_code = code + elif code != response_code: + raise SMTP11Error( + f"multiline response changed status: {line!r}" + ) + lines.append(match.group(3)) + if match.group(2) == b" ": + return code, lines + + def expect(self, expected: int, context: str) -> list[bytes]: + code, lines = self.response() + if code != expected: + raise SMTP11Error( + f"{context}: expected SMTP {expected}, got {code}: {lines!r}" + ) + return lines + + def command(self, command: str) -> tuple[int, list[bytes]]: + self.socket.sendall(command.encode("ascii") + b"\r\n") + return self.response() + + +def require( + result: tuple[int, list[bytes]], expected: int, context: str +) -> list[bytes]: + code, lines = result + if code != expected: + raise SMTP11Error( + f"{context}: expected SMTP {expected}, got {code}: {lines!r}" + ) + return lines + + +def wait_port() -> None: + deadline = time.monotonic() + 30 + consecutive = 0 + while time.monotonic() < deadline: + client: SMTPConnection | None = None + try: + client = SMTPConnection() + client.expect(220, "readiness greeting") + require(client.command("QUIT"), 221, "readiness QUIT") + consecutive += 1 + if consecutive == 2: + return + time.sleep(0.25) + except (OSError, SMTP11Error): + consecutive = 0 + time.sleep(0.1) + finally: + if client is not None: + client.close() + raise SMTP11Error(f"SMTP did not become ready at {HOST}:{PORT}") + + +def restart_bongo() -> None: + run(["sudo", "-n", SYSTEMCTL, "restart", "bongo.service"]) + time.sleep(STARTUP_GRACE) + wait_port() + active = run( + ["sudo", "-n", SYSTEMCTL, "is-active", "bongo.service"] + ).stdout.strip() + if active != "active": + raise SMTP11Error("bongo.service is not active after restart") + + +def make_message(length: int, token: str) -> bytes: + prefix = ( + f"From: SMTP-11 \r\n" + f"To: {USER}@{DOMAIN}\r\n" + f"Subject: SMTP-11 {token}\r\n" + f"Message-ID: <{token}@{DOMAIN}>\r\n" + f"X-Bongo-Test: {token}\r\n" + "\r\n" + "size-boundary=" + ).encode("ascii") + if length < len(prefix) + 2: + raise SMTP11Error( + f"message limit {length} is too small for the test fixture" + ) + message = prefix + b"x" * (length - len(prefix) - 2) + b"\r\n" + if len(message) != length: + raise SMTP11Error("internal message-size construction error") + return message + + +def ehlo(client: SMTPConnection) -> list[bytes]: + client.expect(220, "greeting") + return require( + client.command("EHLO smtp11-client.example"), 250, "EHLO" + ) + + +def start_local_transaction( + client: SMTPConnection, *, size: int | None +) -> None: + suffix = "" if size is None else f" SIZE={size}" + require( + client.command(f"MAIL FROM:{suffix}"), + 250, + "MAIL FROM", + ) + require( + client.command(f"RCPT TO:<{USER}@{DOMAIN}>"), + 250, + "RCPT TO", + ) + + +def send_data(client: SMTPConnection, message: bytes) -> tuple[int, list[bytes]]: + require(client.command("DATA"), 354, "DATA") + client.socket.sendall(message + b".\r\n") + return client.response() + + +def queue_token_ids() -> list[str]: + identifiers: list[str] = [] + for line in queue("list").stdout.splitlines(): + fields = line.split() + if not fields or QUEUE_ID.fullmatch(fields[0]) is None: + continue + message = queue("message", fields[0], binary=True, check=False) + if message.returncode == 0 and TOKEN_PREFIX in message.stdout: + identifiers.append(fields[0]) + return identifiers + + +def open_store() -> StoreClient: + return StoreClient( + USER, + USER, + authPassword=PASSWORD, + host=HOST, + port=STORE_PORT, + ) + + +def store_token_documents(store: StoreClient) -> list[str]: + matches: list[str] = [] + # LIST is a streamed Store response. Consume it completely before issuing + # READ commands on the same connection or payload bytes can be mistaken + # for subsequent response lines. + identifiers = [entry.uid for entry in store.List("/mail/INBOX")] + for identifier in identifiers: + try: + data = store.Read(identifier) + except Exception: + continue + if TOKEN_PREFIX in data: + matches.append(identifier) + return matches + + +def cleanup(store: StoreClient) -> None: + deadline = time.monotonic() + 20 + quiet_since: float | None = None + while time.monotonic() < deadline: + found = False + for document in store_token_documents(store): + found = True + store.Delete(document) + for queue_id in queue_token_ids(): + found = True + queue("delete", queue_id, check=False) + if found: + quiet_since = None + elif quiet_since is None: + quiet_since = time.monotonic() + elif time.monotonic() - quiet_since >= 1: + return + time.sleep(0.25) + + +def wait_for_exact_delivery(store: StoreClient) -> str: + deadline = time.monotonic() + TIMEOUT + while time.monotonic() < deadline: + documents = store_token_documents(store) + if len(documents) > 1: + raise SMTP11Error( + f"exact-limit message delivered more than once: {documents!r}" + ) + if documents: + return documents[0] + time.sleep(0.25) + raise SMTP11Error("exact-limit message did not reach the Store") + + +def normal_checks() -> int: + if not ALLOW_LIVE: + raise SMTP11Error( + "set BONGO_ALLOW_LIVE_SMTP_TEST=1 for the live configuration test" + ) + if not PASSWORD: + raise SMTP11Error("BONGO_TEST_PASSWORD is required") + if LIMIT < 256: + raise SMTP11Error("BONGO_TEST_MESSAGE_LIMIT must be at least 256") + + original = read_configuration() + configured = copy.deepcopy(original) + configured["message_size_limit"] = LIMIT + restored = False + store: StoreClient | None = None + try: + replace_configuration(configured) + restart_bongo() + store = open_store() + cleanup(store) + + client = SMTPConnection() + try: + capabilities = ehlo(client) + if f"SIZE {LIMIT}".encode("ascii") not in capabilities: + raise SMTP11Error( + f"EHLO did not advertise SIZE {LIMIT}: {capabilities!r}" + ) + + lines = require( + client.command( + f"MAIL FROM: SIZE={LIMIT + 1}" + ), + 552, + "declared over-limit MAIL FROM", + ) + if b"size" not in b" ".join(lines).lower(): + raise SMTP11Error( + f"SMTP 552 did not explain the size failure: {lines!r}" + ) + require(client.command("NOOP"), 250, "NOOP after MAIL rejection") + + start_local_transaction(client, size=LIMIT) + require( + send_data(client, make_message(LIMIT, TOKEN)), + 250, + "exact-limit DATA", + ) + require(client.command("NOOP"), 250, "NOOP after exact DATA") + + start_local_transaction(client, size=None) + require( + send_data( + client, + make_message(LIMIT + 1, f"{TOKEN}-over"), + ), + 552, + "undeclared over-limit DATA", + ) + require(client.command("NOOP"), 250, "NOOP after DATA rejection") + require(client.command("QUIT"), 221, "QUIT") + finally: + client.close() + + document = wait_for_exact_delivery(store) + store.Delete(document) + cleanup(store) + if store_token_documents(store) or queue_token_ids(): + raise SMTP11Error("test content remains after cleanup") + + replace_configuration(original) + restart_bongo() + restored = True + finally: + if store is not None: + try: + if not restored: + cleanup(store) + except Exception as error: + print( + f"SMTP-11 cleanup warning: {error}", file=sys.stderr + ) + finally: + try: + store.Quit() + except Exception: + pass + if not restored: + try: + replace_configuration(original) + restart_bongo() + except Exception as error: + print( + f"SMTP-11 restore warning: {error}", file=sys.stderr + ) + + print( + "SMTP-11 SIZE PASS " + f"limit={LIMIT} advertised=yes declared-over=552 " + "exact-data=250 absent-size-over=552 session-recovery=yes " + "queue-clean=yes store-clean=yes config-restored=yes" + ) + return 0 + + +def disk_checks() -> int: + client = SMTPConnection() + try: + ehlo(client) + lines = require( + client.command("MAIL FROM: SIZE=1"), + 452, + "declared-size full-spool MAIL FROM", + ) + if b"storage" not in b" ".join(lines).lower(): + raise SMTP11Error( + f"SMTP 452 did not explain the storage failure: {lines!r}" + ) + require( + client.command("NOOP"), 250, "NOOP after QDSPC rejection" + ) + require(client.command("QUIT"), 221, "QUIT") + finally: + client.close() + + client = SMTPConnection() + try: + ehlo(client) + lines = require( + client.command("MAIL FROM:"), + 452, + "undeclared-size full-spool MAIL FROM", + ) + if b"storage" not in b" ".join(lines).lower(): + raise SMTP11Error( + f"SMTP 452 did not explain the storage failure: {lines!r}" + ) + finally: + client.close() + + print( + "SMTP-11 DISK PASS declared-size=452 no-size=452 " + "enhanced-status=4.3.1" + ) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--disk-only", + action="store_true", + help="require a pre-existing full-spool fixture and test SMTP 452", + ) + arguments = parser.parse_args() + return disk_checks() if arguments.disk_only else normal_checks() + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, SMTP11Error, ValueError) as error: + print(f"SMTP-11 FAIL: {type(error).__name__}: {error}") + raise SystemExit(1) diff --git a/contrib/testing/smtp-size-disk-check.sh b/contrib/testing/smtp-size-disk-check.sh new file mode 100755 index 0000000..2988f3c --- /dev/null +++ b/contrib/testing/smtp-size-disk-check.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# +# Exercise SMTP SIZE boundaries, then overlay the disposable Queue spool with +# a full tmpfs and verify both predeclared and undeclared-size 452 responses. + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +python_check=${BONGO_SMTP_SIZE_CHECK:-"$script_dir/smtp-size-check.py"} +queue_tool=${BONGO_QUEUE_TOOL:-/usr/bin/bongo-queuetool} +service=${BONGO_SERVICE:-bongo.service} +spool=${BONGO_QUEUE_SPOOL:-/var/lib/bongo/spool} +run_as=${BONGO_TEST_RUN_AS:-bongo} +as_root=${BONGO_TEST_AS_ROOT:-sudo -n} + +if [ "$(id -un)" = "$run_as" ]; then + as_bongo= +else + as_bongo="sudo -n -u $run_as" +fi + +work=$(mktemp -d "${TMPDIR:-/tmp}/bongo-smtp11.XXXXXX") +chmod 0755 "$work" +tmpfs="$work/tmpfs" +mkdir "$tmpfs" +chmod 0777 "$tmpfs" +tmpfs_mounted=0 +spool_mounted=0 +fill_open=0 + +restore() +{ + status=$? + if [ "$fill_open" -eq 1 ]; then + exec 9>&- + fill_open=0 + fi + $as_root /usr/bin/systemctl stop "$service" >/dev/null 2>&1 || true + if [ "$spool_mounted" -eq 1 ]; then + $as_root /usr/bin/umount "$spool" >/dev/null 2>&1 || true + fi + if [ "$tmpfs_mounted" -eq 1 ]; then + $as_root /usr/bin/umount "$tmpfs" >/dev/null 2>&1 || true + fi + $as_root /usr/bin/systemctl start "$service" >/dev/null 2>&1 || true + rm -rf "$work" + exit "$status" +} +trap restore EXIT HUP INT TERM + +wait_queue() +{ + attempts=30 + while [ "$attempts" -gt 0 ]; do + if $as_bongo "$queue_tool" space >/dev/null 2>&1; then + return 0 + fi + attempts=$((attempts - 1)) + sleep 1 + done + echo "SMTP-11: Queue did not become ready after service restart" >&2 + return 1 +} + +normal_output=$("$python_check") +printf '%s\n' "$normal_output" + +if [ -n "$($as_bongo "$queue_tool" list)" ]; then + echo "SMTP-11: refusing to hide a non-empty live Queue spool" >&2 + exit 1 +fi + +$as_root /usr/bin/systemctl stop "$service" +$as_root /usr/bin/mount -t tmpfs \ + -o size=8m,mode=0777 bongo-smtp11 "$tmpfs" +tmpfs_mounted=1 +$as_root /usr/bin/mount --bind "$tmpfs" "$spool" +spool_mounted=1 +$as_root /usr/bin/systemctl start "$service" +wait_queue + +set -- $($as_bongo "$queue_tool" space details) +reported_before=$4 +block=$5 +fill_blocks=$((reported_before / block + 1)) +exec 9>"$tmpfs/.smtp11-fill" +fill_open=1 +rm "$tmpfs/.smtp11-fill" +/usr/bin/dd if=/dev/zero bs="$block" count="$fill_blocks" status=none >&9 +$as_root /usr/bin/systemctl restart "$service" +wait_queue + +reported=$($as_bongo "$queue_tool" space) +if [ "$reported" -ne 0 ]; then + echo "SMTP-11: simulated full spool reports $reported usable bytes" >&2 + exit 1 +fi + +disk_output=$("$python_check" --disk-only) +printf '%s\n' "$disk_output" + +printf 'SMTP-11 PASS limit=%s exact=accepted declared-over=552 ' \ + "${BONGO_TEST_MESSAGE_LIMIT:-1024}" +printf 'absent-size-over=552 full-spool=452 reported=%s queue-clean=yes\n' \ + "$reported" diff --git a/docs/release-testing-0.7.md b/docs/release-testing-0.7.md index b053edd..50dcd94 100644 --- a/docs/release-testing-0.7.md +++ b/docs/release-testing-0.7.md @@ -138,7 +138,7 @@ inputs and outputs is absent from the ZIP. | SMTP-27 | DANE takes precedence; MTA-STS enforce/testing/cache and MX wildcard policy through complete BIND/Unbound, Technitium/Technitium, and PowerDNS/PowerDNS Recursor DNS stacks | [PASS](test-evidence/0.7-r1.md#smtp-27) | | | | SMTP-09 | Trusted port-26/Mailman-style return receives DKIM through relayhost hostname, port, TLS, optional authentication, failure, and recovery | [PASS](test-evidence/0.7-r1.md#smtp-09) | | | | SMTP-10 | Open-relay tests reject unauthenticated/untrusted third-party relay | [PASS](test-evidence/0.7-r1.md#smtp-10) | | | -| SMTP-11 | `SIZE` exact limit, over-limit, absent size, and disk-space responses | | | | +| SMTP-11 | `SIZE` exact limit, over-limit, absent size, and disk-space responses | [PASS](test-evidence/0.7-r1.md#smtp-11) | | | | SMTP-12 | `PIPELINING` ordering and error recovery | | | | | SMTP-13 | `8BITMIME` remains byte-correct | | | | | SMTP-14 | DSN `RET`, `ENVID`, `NOTIFY`, and `ORCPT` cases | | | | diff --git a/docs/test-evidence/0.7-r1.md b/docs/test-evidence/0.7-r1.md index 8778810..56a686c 100644 --- a/docs/test-evidence/0.7-r1.md +++ b/docs/test-evidence/0.7-r1.md @@ -2521,6 +2521,48 @@ The focused native `smtp-internal-relay` policy test also passed. The Queue listing was empty before hand-off, no configuration changed, no credential was used, and `bongo.service` remained active. +## SMTP-11 + +Result: **PASS** + +The reusable `smtp-size-disk-check.sh` and `smtp-size-check.py` fixtures +temporarily set the real SMTP Store document's `message_size_limit` to 1024 +bytes. The port-25 EHLO response advertised the exact limit. A declared +`SIZE=1025` was rejected at `MAIL FROM` with `552`; a complete 1024-byte DATA +message was accepted and delivered exactly once to the disposable local +Store; and a 1025-byte DATA message sent without a `SIZE` parameter was read +through its terminator and rejected with `552`. `NOOP` succeeded after each +rejection, proving transaction reset and session recovery rather than a +desynchronised connection. + +The second phase required an empty Queue, stopped Bongo, bind-mounted an +8-MiB tmpfs over the Queue spool, and consumed blocks through an open, +unlinked file until `QDSPC` reported zero usable bytes. With that isolated +spool, both `MAIL FROM ... SIZE=1` and `MAIL FROM` without a size received +`452 4.3.1 Insufficient system storage`. The declared-size path remained +usable after rejection. The latter path exercised failed Queue creation. + +```sh +export BONGO_ALLOW_LIVE_SMTP_TEST=1 +export BONGO_TEST_PASSWORD='the-disposable-test-password' +./contrib/testing/smtp-size-disk-check.sh +``` + +The installed `d782d593` debug build reported on 2026-07-27: + +```text +SMTP-11 SIZE PASS limit=1024 advertised=yes declared-over=552 exact-data=250 absent-size-over=552 session-recovery=yes queue-clean=yes store-clean=yes config-restored=yes +SMTP-11 DISK PASS declared-size=452 no-size=452 enhanced-status=4.3.1 +SMTP-11 PASS limit=1024 exact=accepted declared-over=552 absent-size-over=552 full-spool=452 reported=0 queue-clean=yes +``` + +The trap closed the filler descriptor, stopped the test service, removed both +mounts, restarted Bongo, and deleted its temporary directory. A final check +found no mount over `/var/lib/bongo/spool`, an empty Queue, active +`bongo.service`, and the original unlimited `message_size_limit` value of +zero. The exact-limit Store object and the interrupted first-run fixture were +also removed. + ## SMTP-20 Result: **PASS**