Files
bongo/src/apps/sendmail/sendmail.c
T
Mario Fetka c8b250e9b0
Debian Trixie package bundle / packages (push) Successful in 22m11s
Implement durable C local mail submission
2026-07-24 11:05:19 +02:00

513 lines
17 KiB
C

/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
* </Novell-copyright>
****************************************************************************/
#include <bongomaildrop.h>
#include <ctype.h>
#include <errno.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#ifndef BONGO_POSTDROP_PATH
#define BONGO_POSTDROP_PATH "/usr/sbin/bongopostdrop"
#endif
typedef struct {
char *sender;
char *from_name;
int extract_recipients;
int ignore_dots;
GPtrArray *recipients;
} SendmailOptions;
static void
Usage(const char *program)
{
fprintf(stderr,
"Usage: %s [-i] [-t] [-f sender] [-F name] [recipient ...]\n",
program);
}
static int
AddRecipient(GPtrArray *recipients, const char *value)
{
char *copy;
unsigned int index;
if (value == NULL) return -1;
copy = g_strdup(value);
g_strstrip(copy);
if (copy[0] == '<' && copy[strlen(copy) - 1U] == '>') {
memmove(copy, copy + 1, strlen(copy));
copy[strlen(copy) - 1U] = '\0';
}
if (!BongoMaildropAddressValid(copy)) {
g_free(copy);
return -1;
}
for (index = 0U; index < recipients->len; index++) {
if (strcmp(g_ptr_array_index(recipients, index), copy) == 0) {
g_free(copy);
return 0;
}
}
g_ptr_array_add(recipients, copy);
return 0;
}
static const char *
OptionValue(int argc, char **argv, int *index)
{
if (argv[*index][2] != '\0') return argv[*index] + 2;
if (*index + 1 >= argc) return NULL;
(*index)++;
return argv[*index];
}
static int
ParseOptions(int argc, char **argv, SendmailOptions *options)
{
int index;
memset(options, 0, sizeof(*options));
options->recipients = g_ptr_array_new_with_free_func(g_free);
for (index = 1; index < argc; index++) {
const char *argument = argv[index];
const char *value;
if (strcmp(argument, "--") == 0) {
for (index++; index < argc; index++) {
if (AddRecipient(options->recipients, argv[index]) != 0)
return -1;
}
break;
}
if (argument[0] != '-' || strcmp(argument, "-") == 0) {
if (AddRecipient(options->recipients, argument) != 0) return -1;
} else if (strcmp(argument, "-i") == 0 ||
strcmp(argument, "-oi") == 0) {
options->ignore_dots = 1;
} else if (strcmp(argument, "-t") == 0) {
options->extract_recipients = 1;
} else if (strcmp(argument, "-bm") == 0 ||
strcmp(argument, "-om") == 0 ||
strcmp(argument, "-Am") == 0 ||
strcmp(argument, "-Ac") == 0) {
continue;
} else if (argument[1] == 'f' || argument[1] == 'r') {
value = OptionValue(argc, argv, &index);
if (value == NULL ||
(strcmp(value, "<>") != 0 &&
!BongoMaildropAddressValid(value)))
return -1;
g_free(options->sender);
options->sender = g_strdup(value);
g_strstrip(options->sender);
if (options->sender[0] == '<' &&
options->sender[strlen(options->sender) - 1U] == '>') {
memmove(options->sender, options->sender + 1,
strlen(options->sender));
options->sender[strlen(options->sender) - 1U] = '\0';
}
if (options->sender[0] != '\0' &&
!BongoMaildropAddressValid(options->sender))
return -1;
} else if (argument[1] == 'F') {
value = OptionValue(argc, argv, &index);
if (value == NULL || strchr(value, '\r') != NULL ||
strchr(value, '\n') != NULL)
return -1;
g_free(options->from_name);
options->from_name = g_strdup(value);
} else if (strcmp(argument, "-o") == 0 ||
strcmp(argument, "-O") == 0 ||
strcmp(argument, "-B") == 0 ||
strcmp(argument, "-N") == 0 ||
strcmp(argument, "-R") == 0 ||
strcmp(argument, "-V") == 0) {
if (index + 1 >= argc) return -1;
index++;
} else if ((argument[1] == 'o' || argument[1] == 'O') &&
argument[2] != '\0') {
continue;
} else if (strcmp(argument, "--help") == 0 ||
strcmp(argument, "-h") == 0) {
return 1;
} else {
return -1;
}
}
return 0;
}
static int
ReadMessage(int ignore_dots, GByteArray **message)
{
GByteArray *output = g_byte_array_new();
unsigned char buffer[16384];
if (ignore_dots) {
while (!feof(stdin)) {
size_t count = fread(buffer, 1U, sizeof(buffer), stdin);
if (count != 0U) g_byte_array_append(output, buffer, count);
if (ferror(stdin) ||
output->len > BONGO_MAILDROP_MESSAGE_LIMIT) {
g_byte_array_free(output, TRUE);
errno = ferror(stdin) ? EIO : EFBIG;
return -1;
}
}
} else {
char *line = NULL;
size_t capacity = 0U;
ssize_t count;
while ((count = getline(&line, &capacity, stdin)) >= 0) {
size_t content = (size_t)count;
while (content > 0U &&
(line[content - 1U] == '\n' ||
line[content - 1U] == '\r'))
content--;
if (content == 1U && line[0] == '.') break;
if (content >= 2U && line[0] == '.' && line[1] == '.') {
line++;
count--;
capacity--;
g_byte_array_append(output, (unsigned char *)line,
(size_t)count);
line--;
capacity++;
} else {
g_byte_array_append(output, (unsigned char *)line,
(size_t)count);
}
if (output->len > BONGO_MAILDROP_MESSAGE_LIMIT) {
free(line);
g_byte_array_free(output, TRUE);
errno = EFBIG;
return -1;
}
}
free(line);
if (ferror(stdin)) {
g_byte_array_free(output, TRUE);
errno = EIO;
return -1;
}
}
if (output->len == 0U) {
g_byte_array_free(output, TRUE);
errno = EINVAL;
return -1;
}
*message = output;
return 0;
}
static void
ExtractAddressToken(GPtrArray *recipients, char *token)
{
char *left;
char *right;
char *colon;
char *semicolon;
g_strstrip(token);
left = strrchr(token, '<');
right = left != NULL ? strchr(left + 1, '>') : NULL;
if (left != NULL && right != NULL) {
*right = '\0';
AddRecipient(recipients, left + 1);
return;
}
colon = strchr(token, ':');
if (colon != NULL) token = colon + 1;
semicolon = strrchr(token, ';');
if (semicolon != NULL) *semicolon = '\0';
g_strstrip(token);
if (strchr(token, ' ') == NULL && strchr(token, '\t') == NULL &&
token[0] != '\0')
AddRecipient(recipients, token);
}
static void
ExtractAddresses(GPtrArray *recipients, const char *value)
{
char *copy = g_strdup(value);
char *start = copy;
char *cursor;
int quoted = 0;
int escaped = 0;
int angle = 0;
int comment = 0;
for (cursor = copy; ; cursor++) {
char current = *cursor;
if (escaped) {
escaped = 0;
} else if (current == '\\' && quoted) {
escaped = 1;
} else if (current == '"' && comment == 0) {
quoted = !quoted;
} else if (!quoted && current == '(') {
comment++;
} else if (!quoted && current == ')' && comment > 0) {
comment--;
} else if (!quoted && comment == 0 && current == '<') {
angle++;
} else if (!quoted && comment == 0 && current == '>' && angle > 0) {
angle--;
} else if ((current == ',' && !quoted && comment == 0 &&
angle == 0) || current == '\0') {
*cursor = '\0';
ExtractAddressToken(recipients, start);
start = cursor + 1;
}
if (current == '\0') break;
}
g_free(copy);
}
static int
HeaderName(const unsigned char *line, size_t length, const char *name)
{
size_t name_length = strlen(name);
return length > name_length && line[name_length] == ':' &&
g_ascii_strncasecmp((const char *)line, name, name_length) == 0;
}
static int
PrepareMessage(GByteArray *raw, const SendmailOptions *options,
GByteArray **prepared)
{
GByteArray *output = g_byte_array_new();
GString *address_header = g_string_new(NULL);
size_t offset = 0U;
int skip_header = 0;
int recipient_header = 0;
int has_from = 0;
int in_headers = 1;
while (offset < raw->len) {
size_t end = offset;
size_t content_end;
const unsigned char *line;
size_t length;
int continuation;
while (end < raw->len && raw->data[end] != '\n') end++;
if (end < raw->len) end++;
line = raw->data + offset;
length = end - offset;
content_end = length;
while (content_end > 0U &&
(line[content_end - 1U] == '\n' ||
line[content_end - 1U] == '\r'))
content_end--;
continuation = content_end > 0U &&
(line[0] == ' ' || line[0] == '\t');
if (in_headers && !continuation && recipient_header &&
address_header->len != 0U) {
ExtractAddresses(options->recipients, address_header->str);
g_string_truncate(address_header, 0U);
}
if (in_headers && content_end == 0U) {
if (options->from_name != NULL && !has_from) {
char hostname[256];
struct passwd *password = getpwuid(getuid());
const char *user =
password != NULL ? password->pw_name : "unknown";
if (gethostname(hostname, sizeof(hostname)) != 0)
strcpy(hostname, "localhost");
hostname[sizeof(hostname) - 1U] = '\0';
g_byte_array_append(output, (const unsigned char *)"From: ",
sizeof("From: ") - 1U);
g_byte_array_append(output,
(const unsigned char *)options->from_name,
strlen(options->from_name));
g_byte_array_append(output, (const unsigned char *)" <",
sizeof(" <") - 1U);
g_byte_array_append(output, (const unsigned char *)user,
strlen(user));
g_byte_array_append(output, (const unsigned char *)"@",
1U);
g_byte_array_append(output, (const unsigned char *)hostname,
strlen(hostname));
g_byte_array_append(output,
(const unsigned char *)">\r\n", 3U);
}
in_headers = 0;
skip_header = 0;
recipient_header = 0;
} else if (in_headers && !continuation) {
skip_header = HeaderName(line, content_end, "Bcc");
recipient_header =
options->extract_recipients &&
(HeaderName(line, content_end, "To") ||
HeaderName(line, content_end, "Cc") ||
skip_header);
if (HeaderName(line, content_end, "From")) has_from = 1;
if (recipient_header) {
const unsigned char *colon =
memchr(line, ':', content_end);
if (colon != NULL)
g_string_append_len(address_header,
(const char *)colon + 1,
content_end -
(size_t)(colon + 1 - line));
}
} else if (in_headers && continuation && recipient_header) {
g_string_append_c(address_header, ' ');
g_string_append_len(address_header, (const char *)line,
content_end);
}
if (!in_headers || !skip_header)
g_byte_array_append(output, line, length);
offset = end;
}
if (in_headers && recipient_header && address_header->len != 0U)
ExtractAddresses(options->recipients, address_header->str);
g_string_free(address_header, TRUE);
if (options->recipients->len == 0U) {
g_byte_array_free(output, TRUE);
errno = EINVAL;
return -1;
}
*prepared = output;
return 0;
}
static char *
DefaultSender(void)
{
struct passwd *password = getpwuid(getuid());
const char *user = password != NULL ? password->pw_name : "unknown";
char hostname[256];
if (gethostname(hostname, sizeof(hostname)) != 0)
strcpy(hostname, "localhost");
hostname[sizeof(hostname) - 1U] = '\0';
return g_strdup_printf("%s@%s", user, hostname);
}
static int
Submit(const BongoMaildropSubmission *submission)
{
int descriptors[2];
pid_t child;
FILE *stream;
int status;
struct sigaction ignore;
struct sigaction previous;
if (pipe(descriptors) != 0) return -1;
child = fork();
if (child < 0) {
close(descriptors[0]);
close(descriptors[1]);
return -1;
}
if (child == 0) {
if (dup2(descriptors[0], STDIN_FILENO) < 0) _exit(75);
close(descriptors[0]);
close(descriptors[1]);
execl(BONGO_POSTDROP_PATH, "bongopostdrop", (char *)NULL);
_exit(75);
}
close(descriptors[0]);
memset(&ignore, 0, sizeof(ignore));
ignore.sa_handler = SIG_IGN;
sigemptyset(&ignore.sa_mask);
sigaction(SIGPIPE, &ignore, &previous);
stream = fdopen(descriptors[1], "wb");
if (stream == NULL || BongoMaildropWrite(stream, submission) != 0 ||
fflush(stream) != 0 || fclose(stream) != 0) {
if (stream == NULL) close(descriptors[1]);
sigaction(SIGPIPE, &previous, NULL);
waitpid(child, &status, 0);
return -1;
}
sigaction(SIGPIPE, &previous, NULL);
while (waitpid(child, &status, 0) < 0) {
if (errno != EINTR) return -1;
}
if (!WIFEXITED(status)) {
errno = EIO;
return -1;
}
if (WEXITSTATUS(status) != 0) {
errno = WEXITSTATUS(status) == 64 ? EINVAL : EAGAIN;
return -1;
}
return 0;
}
int
main(int argc, char **argv)
{
SendmailOptions options;
BongoMaildropSubmission submission = { 0 };
GByteArray *raw = NULL;
GByteArray *prepared = NULL;
int result;
result = ParseOptions(argc, argv, &options);
if (result != 0) {
Usage(argv[0]);
if (options.recipients != NULL)
g_ptr_array_free(options.recipients, TRUE);
g_free(options.sender);
g_free(options.from_name);
return result > 0 ? 0 : 64;
}
if (ReadMessage(options.ignore_dots, &raw) != 0 ||
PrepareMessage(raw, &options, &prepared) != 0) {
fprintf(stderr, "bongo-sendmail: invalid message or recipients: %s\n",
strerror(errno));
result = 64;
goto cleanup;
}
submission.uid = getuid();
submission.sender =
options.sender != NULL ? g_strdup(options.sender) : DefaultSender();
submission.recipients = options.recipients;
options.recipients = NULL;
submission.message = g_memdup2(prepared->data, prepared->len);
submission.message_length = prepared->len;
if (Submit(&submission) != 0) {
fprintf(stderr, "bongo-sendmail: local submission failed: %s\n",
strerror(errno));
result = errno == EINVAL ? 64 : 75;
} else {
result = 0;
}
cleanup:
if (raw != NULL) g_byte_array_free(raw, TRUE);
if (prepared != NULL) g_byte_array_free(prepared, TRUE);
BongoMaildropSubmissionClear(&submission);
if (options.recipients != NULL)
g_ptr_array_free(options.recipients, TRUE);
g_free(options.sender);
g_free(options.from_name);
return result;
}