diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:28:47 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:28:47 +0300 |
| commit | 3e9a42db4c6c61e0df8e808db51a834525c4fb0c (patch) | |
| tree | 387cfb80342e7961d32b76955cd490464f946685 /corebinutils | |
| parent | f1d193fda398f32fe0b21d6448d3393661e0982b (diff) | |
| parent | dfe4433f744e8e68d04dee0eab4eb9ae2a884c3e (diff) | |
| download | Project-Tick-3e9a42db4c6c61e0df8e808db51a834525c4fb0c.tar.gz Project-Tick-3e9a42db4c6c61e0df8e808db51a834525c4fb0c.zip | |
Add 'corebinutils/rmail/' from commit 'dfe4433f744e8e68d04dee0eab4eb9ae2a884c3e'
git-subtree-dir: corebinutils/rmail
git-subtree-mainline: f1d193fda398f32fe0b21d6448d3393661e0982b
git-subtree-split: dfe4433f744e8e68d04dee0eab4eb9ae2a884c3e
Diffstat (limited to 'corebinutils')
| -rw-r--r-- | corebinutils/rmail/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/rmail/GNUmakefile | 43 | ||||
| -rw-r--r-- | corebinutils/rmail/README.md | 43 | ||||
| -rw-r--r-- | corebinutils/rmail/rmail.1 | 55 | ||||
| -rw-r--r-- | corebinutils/rmail/rmail.c | 516 | ||||
| -rw-r--r-- | corebinutils/rmail/tests/mock_sendmail.c | 85 | ||||
| -rw-r--r-- | corebinutils/rmail/tests/test.sh | 233 |
7 files changed, 1000 insertions, 0 deletions
diff --git a/corebinutils/rmail/.gitignore b/corebinutils/rmail/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/rmail/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/corebinutils/rmail/GNUmakefile b/corebinutils/rmail/GNUmakefile new file mode 100644 index 0000000000..2d95df56ca --- /dev/null +++ b/corebinutils/rmail/GNUmakefile @@ -0,0 +1,43 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS ?= +CPPFLAGS += -D_POSIX_C_SOURCE=200809L +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/rmail +OBJS := $(OBJDIR)/rmail.o + +HELPER_SRC := $(CURDIR)/tests/mock_sendmail.c +HELPER_BIN := $(OUTDIR)/mock_sendmail + +.PHONY: all clean dirs status test + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/rmail.o: $(CURDIR)/rmail.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/rmail.c" -o "$@" + +$(HELPER_BIN): $(HELPER_SRC) | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) "$<" -o "$@" $(LDLIBS) + +test: $(TARGET) $(HELPER_BIN) + RMAIL_BIN="$(TARGET)" MOCK_SENDMAIL_BIN="$(HELPER_BIN)" \ + sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/rmail/README.md b/corebinutils/rmail/README.md new file mode 100644 index 0000000000..441588a5f2 --- /dev/null +++ b/corebinutils/rmail/README.md @@ -0,0 +1,43 @@ +# rmail + +Standalone Linux-native port of FreeBSD `rmail` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port Strategy + +- The FreeBSD/sendmail-lib implementation was rewritten as a standalone C17 utility. BSD build glue, `sm_io`, `sm_snprintf`, `_PATH_SENDMAIL`, and fixed-size header buffers were removed. +- Input parsing remains manpage-compatible: `rmail` consumes leading `From` / `>From` UUCP envelope lines, compresses relay hops into a single return path, and forwards the remaining message body to a sendmail-compatible MTA. +- Header parsing uses `getline(3)` instead of static 2048-byte buffers, so long UUCP `From` lines are handled without `PATH_MAX`-style or fixed-buffer limits. +- Recipient validation stays strict: operands that begin with `-` are rejected instead of being forwarded to sendmail as flags. + +## BSD to Linux / POSIX Mapping + +| Original mechanism | Linux / POSIX replacement | +|---|---| +| `sm_io_fgets`, `sm_io_fprintf`, `sm_io_open`, `sm_io_close` | `getline(3)`, `fprintf(3)`, `fdopen(3)`, `fclose(3)` | +| `_PATH_SENDMAIL` from sendmail headers | Local default path `/usr/sbin/sendmail` with test override via `RMAIL_SENDMAIL` | +| `sm_snprintf` / sendmail alloc helpers | Local dynamic allocation helpers with `snprintf(3)` sizing | +| Fixed 2048-byte parse buffers | Dynamic `getline(3)` + heap strings | +| `lseek(2)` after `sm_io_tell` on regular stdin | `ftello(3)` to track body start, then `lseek(2)` on `STDIN_FILENO` before `execv(2)` | +| `fork(2)` + `pipe(2)` + `execv(2)` | Preserved directly on Linux | + +## Supported / Unsupported Semantics + +- Supported: `-D domain`, `-T`, strict UUCP `From` / `>From` parsing, `remote from` precedence, bang-path compression, recipient wrapping for comma-containing addresses, and sendmail exit-status propagation. +- Supported: regular-file stdin optimization. If stdin is a seekable regular file, `rmail` seeks to the start of the message body and `exec`s sendmail directly. +- Unsupported by design: GNU long options and GNU option permutation. Parsing is strict FreeBSD/POSIX style. +- Unsupported on Linux: there is no bundled sendmail ABI shim. `rmail` requires an external sendmail-compatible binary at `/usr/sbin/sendmail` or, for test harnesses, `RMAIL_SENDMAIL`. Missing or non-executable mailers fail explicitly. +- Unsupported semantics are not stubbed. If the sendmail-compatible backend exits non-zero or cannot be executed, `rmail` returns an explicit error instead of pretending delivery succeeded. diff --git a/corebinutils/rmail/rmail.1 b/corebinutils/rmail/rmail.1 new file mode 100644 index 0000000000..ed4c34a1cd --- /dev/null +++ b/corebinutils/rmail/rmail.1 @@ -0,0 +1,55 @@ +.\" Copyright (c) 1998-2000 Proofpoint, Inc. and its suppliers. +.\" All rights reserved. +.\" Copyright (c) 1983, 1990 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" By using this file, you agree to the terms and conditions set +.\" forth in the LICENSE file which can be found at the top level of +.\" the sendmail distribution. +.\" +.TH RMAIL 1 "March 3, 2026" +.SH NAME +rmail +\- handle remote mail received via uucp +.SH SYNOPSIS +.B rmail +.RB [ \-D +.IR domain ] +.RB [ \-T ] +.I user ... +.SH DESCRIPTION +.B Rmail +interprets incoming mail received via +uucp(1), +collapsing ``From'' lines in the form generated by +mail.local(8) +into a single line of the form ``return-path!sender'', +and passing the processed mail on to +sendmail(8). +.PP +.B Rmail +is explicitly designed for use with +uucp +and a sendmail-compatible mail transport agent. +.SS Flags +.TP +.B \-D +Use the specified +.I domain +instead of the default domain of ``UUCP''. +.TP +.B \-T +Turn on debugging. +.SH SEE ALSO +uucp(1), +mail.local(8), +sendmail(8) +.SH HISTORY +The +.B rmail +program appeared in +4.2BSD. +.SH BUGS +.B Rmail +should not reside in +/bin. diff --git a/corebinutils/rmail/rmail.c b/corebinutils/rmail/rmail.c new file mode 100644 index 0000000000..7498bff9cc --- /dev/null +++ b/corebinutils/rmail/rmail.c @@ -0,0 +1,516 @@ +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include <ctype.h> +#include <errno.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if __has_include(<sysexits.h>) +#include <sysexits.h> +#endif + +#ifndef EX_OK +#define EX_OK 0 +#endif +#ifndef EX_USAGE +#define EX_USAGE 64 +#endif +#ifndef EX_DATAERR +#define EX_DATAERR 65 +#endif +#ifndef EX_UNAVAILABLE +#define EX_UNAVAILABLE 69 +#endif +#ifndef EX_SOFTWARE +#define EX_SOFTWARE 70 +#endif +#ifndef EX_OSERR +#define EX_OSERR 71 +#endif +#ifndef EX_TEMPFAIL +#define EX_TEMPFAIL 75 +#endif + +#ifndef RMAIL_SENDMAIL_PATH +#define RMAIL_SENDMAIL_PATH "/usr/sbin/sendmail" +#endif + +struct parsed_from_line { + char *system; + char *user; +}; + +static const char usage_message[] = "usage: rmail [-D domain] [-T] user ..."; + +static void +die_with_errno(int code, const char *prefix) +{ + int saved_errno; + + saved_errno = errno; + if (saved_errno == 0) + saved_errno = EIO; + + fprintf(stderr, "rmail: %s: %s\n", prefix, strerror(saved_errno)); + exit(code); +} + +static void +die(int code, const char *fmt, ...) +{ + va_list ap; + + fputs("rmail: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(code); +} + +static void +usage(void) +{ + fprintf(stderr, "%s\n", usage_message); + exit(EX_USAGE); +} + +static void * +xmalloc(size_t size) +{ + void *ptr; + + if (size == 0) + size = 1; + ptr = malloc(size); + if (ptr == NULL) + die(EX_TEMPFAIL, "out of memory"); + return ptr; +} + +static void * +xrealloc(void *ptr, size_t size) +{ + void *next; + + if (size == 0) + size = 1; + next = realloc(ptr, size); + if (next == NULL) + die(EX_TEMPFAIL, "out of memory"); + return next; +} + +static char * +xstrdup(const char *text) +{ + size_t len; + char *copy; + + len = strlen(text) + 1; + copy = xmalloc(len); + memcpy(copy, text, len); + return copy; +} + +static char * +xstrndup(const char *text, size_t len) +{ + char *copy; + + copy = xmalloc(len + 1); + memcpy(copy, text, len); + copy[len] = '\0'; + return copy; +} + +static char * +xasprintf(const char *fmt, ...) +{ + va_list ap; + va_list ap_copy; + int needed; + char *buffer; + + va_start(ap, fmt); + va_copy(ap_copy, ap); + needed = vsnprintf(NULL, 0, fmt, ap_copy); + va_end(ap_copy); + if (needed < 0) { + va_end(ap); + die(EX_SOFTWARE, "snprintf sizing failed"); + } + + buffer = xmalloc((size_t)needed + 1); + if (vsnprintf(buffer, (size_t)needed + 1, fmt, ap) != needed) { + va_end(ap); + die(EX_SOFTWARE, "snprintf formatting failed"); + } + va_end(ap); + + return buffer; +} + +static void +set_owned_string(char **slot, char *value) +{ + free(*slot); + *slot = value; +} + +static void +append_path_component(char **path, size_t *path_len, const char *component) +{ + size_t component_len; + + component_len = strlen(component); + *path = xrealloc(*path, *path_len + component_len + 2); + memcpy(*path + *path_len, component, component_len); + *path_len += component_len; + (*path)[(*path_len)++] = '!'; + (*path)[*path_len] = '\0'; +} + +static const char * +skip_token_end(const char *text) +{ + while (*text != '\0' && !isspace((unsigned char)*text)) + text++; + return text; +} + +static bool +parse_from_line(const char *line, struct parsed_from_line *parsed) +{ + static const char remote_marker[] = " remote from "; + const char *cursor; + const char *token_end; + const char *remote; + char *token; + char *last_bang; + + memset(parsed, 0, sizeof(*parsed)); + + if (strncmp(line, "From ", 5) == 0) + cursor = line + 5; + else if (strncmp(line, ">From ", 6) == 0) + cursor = line + 6; + else + return false; + + if (*cursor == '\0') + die(EX_DATAERR, "corrupted From line: %s", line); + + token_end = skip_token_end(cursor); + if (token_end == cursor) + die(EX_DATAERR, "corrupted From line: %s", line); + + remote = strstr(cursor, remote_marker); + if (remote != NULL) { + const char *site_start; + const char *site_end; + + parsed->user = xstrndup(cursor, (size_t)(token_end - cursor)); + + site_start = remote + strlen(remote_marker); + site_end = skip_token_end(site_start); + if (site_end == site_start) + die(EX_DATAERR, "corrupted From line: %s", line); + parsed->system = xstrndup(site_start, (size_t)(site_end - site_start)); + return true; + } + + token = xstrndup(cursor, (size_t)(token_end - cursor)); + if (token[0] == '!') + die(EX_DATAERR, "bang starts address: %s", token); + + last_bang = strrchr(token, '!'); + if (last_bang == NULL) { + parsed->user = token; + return true; + } + + *last_bang++ = '\0'; + if (*last_bang == '\0') + die(EX_DATAERR, "corrupted From line: %s", line); + + parsed->system = xstrdup(token); + parsed->user = xstrdup(last_bang); + free(token); + return true; +} + +static const char * +resolve_sendmail_path(void) +{ + const char *override; + + override = getenv("RMAIL_SENDMAIL"); + if (override != NULL && *override != '\0') + return override; + return RMAIL_SENDMAIL_PATH; +} + +static void +debug_print_value(bool debug, const char *label, const char *value) +{ + if (!debug) + return; + fprintf(stderr, "%s: %s\n", label, value); +} + +static void +write_stream_or_die(FILE *stream, const char *buffer, size_t length) +{ + if (length == 0) + return; + if (fwrite(buffer, 1, length, stream) != length) + die_with_errno(EX_TEMPFAIL, "write"); +} + +int +main(int argc, char *argv[]) +{ + bool debug; + bool saw_header; + bool stdin_is_regular; + int ch; + int pipe_fds[2]; + int status; + pid_t child_pid; + size_t arg_count; + size_t path_len; + char *domain; + char *line; + char *from_path; + char *from_sys; + char *from_user; + char *protocol_arg; + char *from_arg; + char **sendmail_argv; + const char *sendmail_path; + FILE *child_stdin; + struct stat stdin_stat; + ssize_t line_size; + size_t line_capacity; + off_t body_offset; + char *first_body_line; + size_t first_body_length; + + debug = false; + domain = "UUCP"; + while ((ch = getopt(argc, argv, "D:T")) != -1) { + switch (ch) { + case 'D': + if (optarg == NULL || optarg[0] == '\0') + die(EX_USAGE, "-D requires a non-empty domain"); + domain = optarg; + break; + case 'T': + debug = true; + break; + case '?': + default: + usage(); + } + } + + argc -= optind; + argv += optind; + if (argc < 1) + usage(); + + for (int i = 0; i < argc; i++) { + if (argv[i][0] == '-') + die(EX_USAGE, "dash precedes argument: %s", argv[i]); + } + + if (fstat(STDIN_FILENO, &stdin_stat) != 0) + die_with_errno(EX_OSERR, "stdin"); + stdin_is_regular = S_ISREG(stdin_stat.st_mode); + + line = NULL; + line_capacity = 0; + body_offset = 0; + first_body_line = NULL; + first_body_length = 0; + from_path = NULL; + from_sys = NULL; + from_user = NULL; + path_len = 0; + saw_header = false; + + for (;;) { + char *logical_line; + struct parsed_from_line parsed; + + errno = 0; + line_size = getline(&line, &line_capacity, stdin); + if (line_size < 0) { + if (ferror(stdin)) + die_with_errno(EX_TEMPFAIL, "stdin"); + if (!saw_header) + die(EX_DATAERR, "no data"); + die(EX_DATAERR, "no data"); + } + if (line_size == 0 || line[(size_t)line_size - 1] != '\n') + die(EX_DATAERR, "unterminated input line"); + + logical_line = xstrndup(line, (size_t)line_size - 1); + if (!parse_from_line(logical_line, &parsed)) { + if (!saw_header) + die(EX_DATAERR, "missing or empty From line: %s", + logical_line); + first_body_line = xstrdup(line); + first_body_length = (size_t)line_size; + free(logical_line); + break; + } + free(logical_line); + saw_header = true; + + if (parsed.system != NULL) { + debug_print_value(debug, strstr(line, " remote from ") != NULL ? + "remote from" : "bang", parsed.system); + if (from_sys == NULL) + from_sys = xstrdup(parsed.system); + append_path_component(&from_path, &path_len, parsed.system); + } + + if (parsed.user[0] == '\0') + set_owned_string(&from_user, xstrdup("<>")); + else + set_owned_string(&from_user, parsed.user); + parsed.user = NULL; + + debug_print_value(debug, "from_sys", + from_sys != NULL ? from_sys : "(none)"); + if (from_path != NULL) + debug_print_value(debug, "from_path", from_path); + debug_print_value(debug, "from_user", from_user); + + free(parsed.system); + free(parsed.user); + + if (stdin_is_regular) { + body_offset = ftello(stdin); + if (body_offset < 0) + die_with_errno(EX_TEMPFAIL, "stdin"); + } else if (body_offset == 0) { + body_offset = 1; + } + } + + if (from_user == NULL) + set_owned_string(&from_user, xstrdup("<>")); + +#ifdef QUEUE_ONLY + { + const char *delivery_mode = "-odq"; +#else + { + const char *delivery_mode = "-odi"; +#endif + protocol_arg = NULL; + from_arg = NULL; + sendmail_argv = NULL; + + if (from_sys == NULL) { + protocol_arg = xasprintf("-p%s", domain); + } else if (strchr(from_sys, '.') == NULL) { + protocol_arg = xasprintf("-p%s:%s.%s", + domain, from_sys, domain); + } else { + protocol_arg = xasprintf("-p%s:%s", domain, from_sys); + } + + from_arg = xasprintf("-f%s%s", + from_path != NULL ? from_path : "", from_user); + + arg_count = (size_t)argc + 8; + sendmail_argv = xmalloc(sizeof(*sendmail_argv) * arg_count); + sendmail_path = resolve_sendmail_path(); + + sendmail_argv[0] = (char *)sendmail_path; + sendmail_argv[1] = "-G"; + sendmail_argv[2] = "-oee"; + sendmail_argv[3] = (char *)delivery_mode; + sendmail_argv[4] = "-oi"; + sendmail_argv[5] = protocol_arg; + sendmail_argv[6] = from_arg; + for (int i = 0; i < argc; i++) { + if (strchr(argv[i], ',') != NULL && strchr(argv[i], '<') == NULL) + sendmail_argv[7 + i] = xasprintf("<%s>", argv[i]); + else + sendmail_argv[7 + i] = argv[i]; + } + sendmail_argv[7 + argc] = NULL; + } + + if (debug) { + fprintf(stderr, "Sendmail arguments:\n"); + for (size_t i = 0; sendmail_argv[i] != NULL; i++) + fprintf(stderr, "\t%s\n", sendmail_argv[i]); + } + + if (stdin_is_regular) { + clearerr(stdin); + if (lseek(STDIN_FILENO, body_offset, SEEK_SET) == (off_t)-1) + die_with_errno(EX_TEMPFAIL, "stdin seek"); + execv(sendmail_path, sendmail_argv); + die(EX_UNAVAILABLE, "%s: %s", sendmail_path, strerror(errno)); + } + + if (pipe(pipe_fds) != 0) + die_with_errno(EX_OSERR, "pipe"); + + child_pid = fork(); + if (child_pid < 0) + die_with_errno(EX_OSERR, "fork"); + if (child_pid == 0) { + if (dup2(pipe_fds[0], STDIN_FILENO) < 0) { + fprintf(stderr, "rmail: dup2: %s\n", strerror(errno)); + _exit(EX_OSERR); + } + close(pipe_fds[0]); + close(pipe_fds[1]); + execv(sendmail_path, sendmail_argv); + fprintf(stderr, "rmail: %s: %s\n", sendmail_path, strerror(errno)); + _exit(EX_UNAVAILABLE); + } + + close(pipe_fds[0]); + child_stdin = fdopen(pipe_fds[1], "w"); + if (child_stdin == NULL) + die_with_errno(EX_OSERR, "fdopen"); + + write_stream_or_die(child_stdin, first_body_line, first_body_length); + while ((line_size = getline(&line, &line_capacity, stdin)) >= 0) + write_stream_or_die(child_stdin, line, (size_t)line_size); + if (ferror(stdin)) + die_with_errno(EX_TEMPFAIL, "stdin"); + if (fclose(child_stdin) != 0) + die_with_errno(EX_OSERR, "pipe close"); + + if (waitpid(child_pid, &status, 0) < 0) + die_with_errno(EX_OSERR, sendmail_path); + if (!WIFEXITED(status)) + die(EX_OSERR, "%s: did not terminate normally", sendmail_path); + if (WEXITSTATUS(status) != 0) + die(WEXITSTATUS(status), "%s: terminated with %d (non-zero) status", + sendmail_path, WEXITSTATUS(status)); + + free(line); + free(first_body_line); + free(from_path); + free(from_sys); + free(from_user); + + return EX_OK; +} diff --git a/corebinutils/rmail/tests/mock_sendmail.c b/corebinutils/rmail/tests/mock_sendmail.c new file mode 100644 index 0000000000..0e1403c7ee --- /dev/null +++ b/corebinutils/rmail/tests/mock_sendmail.c @@ -0,0 +1,85 @@ +#define _POSIX_C_SOURCE 200809L + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static const char * +require_env(const char *name) +{ + const char *value; + + value = getenv(name); + if (value == NULL || value[0] == '\0') { + fprintf(stderr, "mock_sendmail: missing %s\n", name); + exit(111); + } + return value; +} + +int +main(int argc, char *argv[]) +{ + const char *argv_path; + const char *stdin_path; + const char *exit_env; + FILE *argv_file; + FILE *stdin_file; + char buffer[4096]; + size_t nread; + + argv_path = require_env("RMAIL_SENDMAIL_LOG"); + stdin_path = require_env("RMAIL_SENDMAIL_INPUT"); + + argv_file = fopen(argv_path, "w"); + if (argv_file == NULL) { + fprintf(stderr, "mock_sendmail: fopen(%s): %s\n", + argv_path, strerror(errno)); + return 111; + } + for (int i = 0; i < argc; i++) { + if (fprintf(argv_file, "%s\n", argv[i]) < 0) { + fprintf(stderr, "mock_sendmail: fprintf(%s): %s\n", + argv_path, strerror(errno)); + fclose(argv_file); + return 111; + } + } + if (fclose(argv_file) != 0) { + fprintf(stderr, "mock_sendmail: fclose(%s): %s\n", + argv_path, strerror(errno)); + return 111; + } + + stdin_file = fopen(stdin_path, "w"); + if (stdin_file == NULL) { + fprintf(stderr, "mock_sendmail: fopen(%s): %s\n", + stdin_path, strerror(errno)); + return 111; + } + while ((nread = fread(buffer, 1, sizeof(buffer), stdin)) > 0) { + if (fwrite(buffer, 1, nread, stdin_file) != nread) { + fprintf(stderr, "mock_sendmail: fwrite(%s): %s\n", + stdin_path, strerror(errno)); + fclose(stdin_file); + return 111; + } + } + if (ferror(stdin)) { + fprintf(stderr, "mock_sendmail: fread(stdin): %s\n", + strerror(errno)); + fclose(stdin_file); + return 111; + } + if (fclose(stdin_file) != 0) { + fprintf(stderr, "mock_sendmail: fclose(%s): %s\n", + stdin_path, strerror(errno)); + return 111; + } + + exit_env = getenv("RMAIL_SENDMAIL_EXIT"); + if (exit_env != NULL && exit_env[0] != '\0') + return (int)strtol(exit_env, NULL, 10); + return 0; +} diff --git a/corebinutils/rmail/tests/test.sh b/corebinutils/rmail/tests/test.sh new file mode 100644 index 0000000000..04605175a3 --- /dev/null +++ b/corebinutils/rmail/tests/test.sh @@ -0,0 +1,233 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +RMAIL_BIN=${RMAIL_BIN:-"$ROOT/out/rmail"} +MOCK_SENDMAIL_BIN=${MOCK_SENDMAIL_BIN:-"$ROOT/out/mock_sendmail"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/rmail-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +SENDMAIL_LOG="$WORKDIR/sendmail-argv" +SENDMAIL_INPUT="$WORKDIR/sendmail-input" +LAST_STATUS=0 +LAST_STDOUT= +LAST_STDERR= + +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +export LC_ALL=C + +EX_USAGE=64 +EX_DATAERR=65 +EX_UNAVAILABLE=69 + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_eq() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" != "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected ---" >&2 + printf '%s' "$expected" >&2 + printf '\n%s\n' "--- actual ---" >&2 + printf '%s' "$actual" >&2 + printf '\n' >&2 + exit 1 + fi +} + +assert_contains() { + name=$1 + text=$2 + pattern=$3 + case $text in + *"$pattern"*) ;; + *) fail "$name" ;; + esac +} + +assert_empty() { + name=$1 + text=$2 + if [ -n "$text" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected empty ---" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s' "$text" >&2 + printf '\n' >&2 + exit 1 + fi +} + +assert_status() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" -ne "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected status: $expected" >&2 + printf '%s\n' "actual status: $actual" >&2 + exit 1 + fi +} + +run_capture() { + rm -f "$STDOUT_FILE" "$STDERR_FILE" + if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +run_with_input() { + input=$1 + shift + rm -f "$STDOUT_FILE" "$STDERR_FILE" + if printf '%b' "$input" | "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +run_with_regular_file() { + input_file=$1 + shift + rm -f "$STDOUT_FILE" "$STDERR_FILE" + if "$@" <"$input_file" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +read_file() { + cat "$1" +} + +[ -x "$RMAIL_BIN" ] || fail "missing binary: $RMAIL_BIN" +[ -x "$MOCK_SENDMAIL_BIN" ] || fail "missing mock sendmail: $MOCK_SENDMAIL_BIN" + +export RMAIL_SENDMAIL="$MOCK_SENDMAIL_BIN" +export RMAIL_SENDMAIL_LOG="$SENDMAIL_LOG" +export RMAIL_SENDMAIL_INPUT="$SENDMAIL_INPUT" +unset RMAIL_SENDMAIL_EXIT + +run_capture "$RMAIL_BIN" +assert_status "usage status" "$EX_USAGE" "$LAST_STATUS" +assert_empty "usage stdout" "$LAST_STDOUT" +assert_eq "usage stderr" "usage: rmail [-D domain] [-T] user ..." "$LAST_STDERR" + +run_capture "$RMAIL_BIN" -D '' +assert_status "empty domain status" "$EX_USAGE" "$LAST_STATUS" +assert_empty "empty domain stdout" "$LAST_STDOUT" +assert_eq "empty domain stderr" "rmail: -D requires a non-empty domain" "$LAST_STDERR" + +run_capture "$RMAIL_BIN" -- -bad@example.test +assert_status "dash recipient status" "$EX_USAGE" "$LAST_STATUS" +assert_empty "dash recipient stdout" "$LAST_STDOUT" +assert_eq "dash recipient stderr" "rmail: dash precedes argument: -bad@example.test" "$LAST_STDERR" + +run_with_input '' "$RMAIL_BIN" user@example.test +assert_status "no data status" "$EX_DATAERR" "$LAST_STATUS" +assert_empty "no data stdout" "$LAST_STDOUT" +assert_eq "no data stderr" "rmail: no data" "$LAST_STDERR" + +run_with_input 'Subject: hi\n\nbody\n' "$RMAIL_BIN" user@example.test +assert_status "missing from status" "$EX_DATAERR" "$LAST_STATUS" +assert_empty "missing from stdout" "$LAST_STDOUT" +assert_eq "missing from stderr" "rmail: missing or empty From line: Subject: hi" "$LAST_STDERR" + +run_with_input 'From !bad Tue Jan 1 00:00:00 2025\n\nbody\n' "$RMAIL_BIN" user@example.test +assert_status "bang starts status" "$EX_DATAERR" "$LAST_STATUS" +assert_empty "bang starts stdout" "$LAST_STDOUT" +assert_eq "bang starts stderr" "rmail: bang starts address: !bad" "$LAST_STDERR" + +run_with_input 'From user Tue Jan 1 00:00:00 2025' "$RMAIL_BIN" user@example.test +assert_status "unterminated header status" "$EX_DATAERR" "$LAST_STATUS" +assert_empty "unterminated header stdout" "$LAST_STDOUT" +assert_eq "unterminated header stderr" "rmail: unterminated input line" "$LAST_STDERR" + +PIPE_INPUT='From host1!host2!alice Tue Jan 1 00:00:00 2025 +>From bob Tue Jan 1 00:01:00 2025 remote from relay.example + +hello from pipe +' +run_with_input "$PIPE_INPUT" "$RMAIL_BIN" first@example.test second,third@example.test '<already@wrapped>' +assert_status "pipe happy status" 0 "$LAST_STATUS" +assert_empty "pipe happy stdout" "$LAST_STDOUT" +assert_empty "pipe happy stderr" "$LAST_STDERR" +assert_eq "pipe sendmail argv" "$MOCK_SENDMAIL_BIN +-G +-oee +-odi +-oi +-pUUCP:host1!host2.UUCP +-fhost1!host2!relay.example!bob +first@example.test +<second,third@example.test> +<already@wrapped>" "$(read_file "$SENDMAIL_LOG")" +assert_eq "pipe sendmail input" " +hello from pipe" "$(read_file "$SENDMAIL_INPUT")" + +REGULAR_INPUT="$WORKDIR/regular-input" +cat >"$REGULAR_INPUT" <<'EOF' +From charlie Tue Jan 1 00:00:00 2025 remote from mailhub + +hello from regular file +EOF +run_with_regular_file "$REGULAR_INPUT" "$RMAIL_BIN" -D LINUX target@example.test +assert_status "regular happy status" 0 "$LAST_STATUS" +assert_empty "regular happy stdout" "$LAST_STDOUT" +assert_empty "regular happy stderr" "$LAST_STDERR" +assert_eq "regular sendmail argv" "$MOCK_SENDMAIL_BIN +-G +-oee +-odi +-oi +-pLINUX:mailhub.LINUX +-fmailhub!charlie +target@example.test" "$(read_file "$SENDMAIL_LOG")" +assert_eq "regular sendmail input" " +hello from regular file" "$(read_file "$SENDMAIL_INPUT")" + +DEBUG_INPUT='From user Tue Jan 1 00:00:00 2025 remote from relay + +debug body +' +run_with_input "$DEBUG_INPUT" "$RMAIL_BIN" -T user@example.test +assert_status "debug status" 0 "$LAST_STATUS" +assert_empty "debug stdout" "$LAST_STDOUT" +assert_contains "debug stderr remote" "$LAST_STDERR" "remote from: relay" +assert_contains "debug stderr from_sys" "$LAST_STDERR" "from_sys: relay" +assert_contains "debug stderr from_path" "$LAST_STDERR" "from_path: relay!" +assert_contains "debug stderr from_user" "$LAST_STDERR" "from_user: user" +assert_contains "debug stderr args" "$LAST_STDERR" "Sendmail arguments:" + +export RMAIL_SENDMAIL_EXIT=23 +run_with_input 'From user Tue Jan 1 00:00:00 2025\n\nbody\n' "$RMAIL_BIN" user@example.test +assert_status "sendmail exit status propagation" 23 "$LAST_STATUS" +assert_empty "sendmail exit stdout" "$LAST_STDOUT" +assert_contains "sendmail exit stderr" "$LAST_STDERR" "terminated with 23 (non-zero) status" +unset RMAIL_SENDMAIL_EXIT + +export RMAIL_SENDMAIL="$WORKDIR/missing-sendmail" +run_with_input 'From user Tue Jan 1 00:00:00 2025\n\nbody\n' "$RMAIL_BIN" user@example.test +assert_status "missing sendmail status" "$EX_UNAVAILABLE" "$LAST_STATUS" +assert_empty "missing sendmail stdout" "$LAST_STDOUT" +assert_contains "missing sendmail stderr" "$LAST_STDERR" "$WORKDIR/missing-sendmail" + +printf '%s\n' "PASS" |
