513 lines
17 KiB
C
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;
|
|
}
|