Implement durable C local mail submission
Debian Trixie package bundle / packages (push) Successful in 22m11s

This commit is contained in:
Mario Fetka
2026-07-24 11:05:19 +02:00
parent 785c89e42c
commit c8b250e9b0
23 changed files with 1457 additions and 810 deletions
+1
View File
@@ -63,6 +63,7 @@ set(XPL_DEFAULT_WORK_DIR ${XPL_DEFAULT_STATE_DIR}/work)
set(XPL_DEFAULT_CACHE_DIR ${XPL_DEFAULT_STATE_DIR}/cache)
set(XPL_DEFAULT_SCMS_DIR ${XPL_DEFAULT_STATE_DIR}/scms)
set(XPL_DEFAULT_SPOOL_DIR ${XPL_DEFAULT_STATE_DIR}/spool)
set(XPL_DEFAULT_MAILDROP_DIR ${XPL_DEFAULT_STATE_DIR}/maildrop)
set(XPL_DEFAULT_STORE_SYSTEM_DIR ${XPL_DEFAULT_STATE_DIR}/system)
set(XPL_DEFAULT_SYSTEM_DIR ${XPL_DEFAULT_STATE_DIR}/system)
set(XPL_DEFAULT_MAIL_DIR ${XPL_DEFAULT_STATE_DIR}/users)
+1
View File
@@ -52,6 +52,7 @@
#cmakedefine XPL_DEFAULT_LIB_DIR "@XPL_DEFAULT_LIB_DIR@"
#cmakedefine XPL_DEFAULT_CONF_DIR "@XPL_DEFAULT_CONF_DIR@"
#cmakedefine XPL_DEFAULT_CONFIG_DIR "@XPL_DEFAULT_CONFIG_DIR@"
#cmakedefine XPL_DEFAULT_MAILDROP_DIR "@XPL_DEFAULT_MAILDROP_DIR@"
#cmakedefine XPL_DEFAULT_ALIASES_DIR "@XPL_DEFAULT_ALIASES_DIR@"
#cmakedefine XPL_DEFAULT_SSL_DIR "@XPL_DEFAULT_SSL_DIR@"
#cmakedefine XPL_DEFAULT_DKIM_DIR "@XPL_DEFAULT_DKIM_DIR@"
+1
View File
@@ -95,6 +95,7 @@ assert_path "$RUNTIME_DIR" 750 bongo:bongo
assert_path "$STATE_DIR" 750 bongo:bongo
assert_path "$STATE_DIR/dbf" 750 bongo:bongo
assert_path "$STATE_DIR/dbf/cookies" 700 bongo:bongo
assert_path "$STATE_DIR/maildrop" 1730 bongo:bongopostdrop
assert_path /etc/bongo/ssl.d 750 root:bongo
echo "PASS tmpfiles recreated volatile and persistent directory ownership"
+11 -2
View File
@@ -4,9 +4,18 @@ set -e
if [ "$1" = configure ]; then
if command -v systemd-sysusers >/dev/null 2>&1; then
systemd-sysusers bongo-user.conf
elif ! getent passwd bongo >/dev/null 2>&1; then
adduser --system --group --home /var/lib/bongo --no-create-home bongo
else
if ! getent group bongopostdrop >/dev/null 2>&1; then
addgroup --system bongopostdrop
fi
if ! getent passwd bongo >/dev/null 2>&1; then
adduser --system --group --home /var/lib/bongo \
--no-create-home bongo
fi
adduser bongo bongopostdrop
fi
chown root:bongopostdrop /usr/sbin/bongopostdrop
chmod 2755 /usr/sbin/bongopostdrop
fi
#DEBHELPER#
+5
View File
@@ -4,6 +4,11 @@ set -eu
test -x /usr/sbin/bongo-manager
test -x /usr/sbin/bongo-admin
test -x /usr/sbin/bongo-config
test -x /usr/sbin/bongo-sendmail
test -x /usr/sbin/bongopostdrop
[ "$(stat -c '%a %U:%G' /usr/sbin/bongopostdrop)" = \
"2755 root:bongopostdrop" ]
getent group bongopostdrop >/dev/null
python3 -c 'import bongo, bongo_web, libbongo'
test -f /lib/systemd/system/bongo.service ||
test -f /usr/lib/systemd/system/bongo.service
+47
View File
@@ -0,0 +1,47 @@
/****************************************************************************
* <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>
****************************************************************************/
#ifndef BONGO_MAILDROP_H
#define BONGO_MAILDROP_H
#include <stdint.h>
#include <stdio.h>
#include <sys/types.h>
#include <glib.h>
#define BONGO_MAILDROP_ADDRESS_LIMIT 998U
#define BONGO_MAILDROP_RECIPIENT_LIMIT 1000U
#define BONGO_MAILDROP_MESSAGE_LIMIT (256U * 1024U * 1024U)
typedef struct {
uid_t uid;
char *sender;
GPtrArray *recipients;
unsigned char *message;
size_t message_length;
} BongoMaildropSubmission;
int BongoMaildropAddressValid(const char *address);
int BongoMaildropWrite(FILE *stream, const BongoMaildropSubmission *submission);
int BongoMaildropRead(FILE *stream, size_t message_limit,
unsigned int recipient_limit,
BongoMaildropSubmission *submission);
void BongoMaildropSubmissionClear(BongoMaildropSubmission *submission);
#endif
+2
View File
@@ -19,4 +19,6 @@
# * </Novell-copyright>
# ****************************************************************************/
g bongopostdrop - -
u @BONGO_USER@ - "Bongo mail and calendar server" @XPL_DEFAULT_STATE_DIR@ -
m @BONGO_USER@ bongopostdrop
+1
View File
@@ -27,6 +27,7 @@ d @XPL_DEFAULT_CACHE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
d @XPL_DEFAULT_CACHE_DIR@/mta-sts 0750 @BONGO_USER@ @BONGO_USER@ -
d @XPL_DEFAULT_SCMS_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
d @XPL_DEFAULT_SPOOL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
d @XPL_DEFAULT_MAILDROP_DIR@ 1730 @BONGO_USER@ bongopostdrop -
d @XPL_DEFAULT_SYSTEM_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
d @XPL_DEFAULT_MAIL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ -
d @XPL_DEFAULT_COOKIE_DIR@ 0700 @BONGO_USER@ @BONGO_USER@ -
+4
View File
@@ -32,11 +32,15 @@ foreach(page IN LISTS BONGO_MAN1_PAGES)
endforeach()
configure_file(bongoagents.8
"${CMAKE_CURRENT_BINARY_DIR}/bongoagents.8" @ONLY)
configure_file(bongopostdrop.8
"${CMAKE_CURRENT_BINARY_DIR}/bongopostdrop.8" @ONLY)
install(FILES ${BONGO_CONFIGURED_MAN1_PAGES}
DESTINATION ${MAN_INSTALL_DIR}/man1)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/bongoagents.8"
DESTINATION ${MAN_INSTALL_DIR}/man8)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/bongopostdrop.8"
DESTINATION ${MAN_INSTALL_DIR}/man8)
foreach(agent IN LISTS BONGO_AGENT_MAN8_NAMES)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/bongoagents.8"
DESTINATION ${MAN_INSTALL_DIR}/man8
+26 -5
View File
@@ -9,7 +9,12 @@ bongo-sendmail \(em submit a message to the local Bongo Queue
.PP
The
.B bongo-sendmail
command attempts to implement an LSB-compliant sendmail interface direct to the Bongo Queue agent. Most useful options are implemented; some are not.
command provides the common sendmail-compatible local submission interface.
It passes a bounded and validated request to
.BR bongopostdrop (8),
which commits it atomically to Bongo's protected local maildrop. The Queue
agent then imports the message through its normal incoming path. No Bongo
system credential and no SMTP listener are required.
.PP
.B bongo-sendmail
will continue to read input from standard input until it sees either a dot alone on a line, or reaches the end of file.
@@ -17,12 +22,17 @@ will continue to read input from standard input until it sees either a dot alone
.PP
A summary of options is included below.
.TP
.BI \-r " mail_from"
Set the From: header of the mail being sent.
.BI \-r " envelope_from"
Set the sender address on the mail envelope. This is an alias for
.BR \-f .
.TP
.BI \-F " env_from"
.BI \-f " envelope_from"
Set the sender address on the mail envelope.
.TP
.BI \-F " full_name"
Supply the display name used for a generated From header when the message has
no From header.
.TP
.BR \-i
Read the entire standard input until the end of the file.
.TP
@@ -33,7 +43,18 @@ Read from standard input until a dot appears alone on a line.
-oi is equivalent to -i; other arguments are ignored.
.TP
.BR \-t
Extract recipient addresses from the mail body.
Extract recipient addresses from the To, Cc, and Bcc headers. Bcc headers
and their continuations are removed from the stored message.
.SH "SECURITY"
The frontend never reads Bongo's Queue credential. The installed
.B bongopostdrop
helper is set-group-ID to the dedicated
.B bongopostdrop
group and has access only to the local maildrop directory. Maildrop records
are treated as hostile input and validated again by the Queue agent.
.SH "SEE ALSO"
.BR bongopostdrop (8),
.BR bongoqueue (8)
.SH "AUTHOR"
.PP
This manual page was originally written by Alex Hudson <home@alexhudson.com> for the
+31
View File
@@ -0,0 +1,31 @@
.TH BONGOPOSTDROP 8 "July 2026" "Bongo @BONGO_V_STR@" "Bongo local maildrop"
.SH NAME
bongopostdrop \(em atomically commit a local Bongo submission
.SH SYNOPSIS
.B bongopostdrop
.SH DESCRIPTION
.B bongopostdrop
is the restricted privilege-separation helper used by
.BR bongo-sendmail (1).
It accepts only Bongo's length-delimited local-submission protocol on standard
input, validates all envelope fields and bounds, and atomically commits the
record to the protected Bongo maildrop.
.PP
This command is not a general-purpose Queue client. It does not read the
Bongo system credential, contact SMTP, or accept command-line paths. The
Bongo Queue agent claims and revalidates each record before creating its
private Queue control and data files.
.SH SECURITY
The installed executable must be owned by
.B root:bongopostdrop
and have mode 2755. The maildrop directory is owned by the Bongo service
account and the
.B bongopostdrop
group with mode 1730. Do not grant users direct membership in that group.
.SH FILES
.TP
.I @XPL_DEFAULT_MAILDROP_DIR@
Protected local maildrop.
.SH SEE ALSO
.BR bongo-sendmail (1),
.BR bongoqueue (8)
+217
View File
@@ -23,8 +23,12 @@
#include <config.h>
#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdint.h>
#include <sys/stat.h>
#include <unistd.h>
#include "queue.h"
#include "collected-count.h"
@@ -36,6 +40,7 @@
#include "spool-file.h"
#include "store-delivery-format.h"
#include "delivery-policy.h"
#include <bongomaildrop.h>
static BOOL
QueueFormatBuffer(char *destination, size_t destinationSize,
@@ -105,6 +110,216 @@ uint32_t IntHash(const void *key);
int IntCmp(const void *key1, const void *key2);
static int HandleDSN(FILE *data, FILE *control);
static BOOL ProcessQueueEntry(char *entryIn);
static void
StartImportedQueueEntry(unsigned long id)
{
char entry[16];
int result;
if (!QueueFormat(entry, "%03d%07lx", Q_INCOMING, id)) return;
if (XplSafeRead(Queue.activeWorkers) < Conf.maxConcurrentWorkers) {
XplThreadID thread_id;
XplBeginCountedThread(&thread_id, ProcessQueueEntry, STACKSPACE_Q,
MemStrdup(entry), result,
Queue.activeWorkers);
} else {
Queue.restartNeeded = TRUE;
}
}
/*
* Convert one hostile, user-owned maildrop record into private Queue files.
* The record has already been renamed to an unpredictable pickup name, so a
* submitter cannot reach it through the maildrop directory while it is read.
*/
static int
ImportMaildropFile(const char *path)
{
BongoMaildropSubmission submission = { 0 };
struct stat details;
char control_work[XPL_MAX_PATH + 1];
char control_path[XPL_MAX_PATH + 1];
char data_work[XPL_MAX_PATH + 1];
char data_path[XPL_MAX_PATH + 1];
char date[80];
FILE *drop = NULL;
FILE *control = NULL;
FILE *data = NULL;
unsigned long id;
unsigned int index;
int invalid = 0;
int saved_error = 0;
drop = fopen(path, "rb");
if (drop == NULL) return -1;
if (fstat(fileno(drop), &details) != 0 || !S_ISREG(details.st_mode) ||
details.st_nlink != 1 ||
details.st_size <= 0 ||
(uintmax_t)details.st_size >
(uintmax_t)BONGO_MAILDROP_MESSAGE_LIMIT +
((uintmax_t)BONGO_MAILDROP_RECIPIENT_LIMIT *
(BONGO_MAILDROP_ADDRESS_LIMIT + 4U)) + 4096U ||
BongoMaildropRead(drop, BONGO_MAILDROP_MESSAGE_LIMIT,
BONGO_MAILDROP_RECIPIENT_LIMIT,
&submission) != 0 ||
details.st_uid != submission.uid) {
invalid = errno == EIO || errno == ENOMEM ? 0 : 1;
saved_error = errno != 0 ? errno : EINVAL;
fclose(drop);
BongoMaildropSubmissionClear(&submission);
errno = saved_error;
return invalid ? 1 : -1;
}
fclose(drop);
drop = NULL;
XplMutexLock(Queue.queueIDLock);
id = Queue.queueID++ & ((1UL << 28) - 1UL);
XplMutexUnlock(Queue.queueIDLock);
if (!QueueFormat(control_work, "%s/c%07lx.local",
Conf.spoolPath, id) ||
!QueueFormat(control_path, "%s/c%07lx.%03d",
Conf.spoolPath, id, Q_INCOMING) ||
!QueueFormat(data_work, "%s/d%07lx.local",
Conf.spoolPath, id) ||
!QueueFormat(data_path, "%s/d%07lx.msg",
Conf.spoolPath, id)) {
saved_error = ENAMETOOLONG;
goto fail;
}
control = fdopen(open(control_work,
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW,
0600), "wb");
if (control == NULL) {
saved_error = errno;
goto fail;
}
data = fdopen(open(data_work,
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW,
0600), "wb");
if (data == NULL) {
saved_error = errno;
goto fail;
}
MsgGetRFC822Date(-1, 0, date);
if (BongoQueueFilePrintf(
control, QUEUES_DATE"%" PRIdMAX "\r\n",
(intmax_t)time(NULL)) != 0 ||
BongoQueueFilePrintf(control, QUEUES_FROM"%s -\r\n",
submission.sender[0] != '\0' ?
submission.sender : "-") != 0) {
saved_error = errno;
goto fail;
}
for (index = 0U; index < submission.recipients->len; index++) {
const char *recipient =
g_ptr_array_index(submission.recipients, index);
if (BongoQueueFilePrintf(control, QUEUES_RECIP_REMOTE"%s %s 2\r\n",
recipient, recipient) != 0) {
saved_error = errno;
goto fail;
}
}
if (BongoQueueFilePrintf(
data,
"Received: from local (uid %lu) by %s\r\n"
"\twith Bongo local submission; %s\r\n",
(unsigned long)submission.uid, BongoGlobals.hostname, date) != 0 ||
BongoQueueFileWrite(data, submission.message,
submission.message_length) != 0 ||
fsync(fileno(data)) != 0 || fsync(fileno(control)) != 0 ||
BongoQueueFileFinish(&data) != 0 ||
BongoQueueFileFinish(&control) != 0) {
saved_error = errno != 0 ? errno : EIO;
goto fail;
}
if (rename(data_work, data_path) != 0) {
saved_error = errno;
goto fail;
}
if (rename(control_work, control_path) != 0) {
saved_error = errno;
unlink(data_path);
goto fail;
}
XplSafeIncrement(Queue.queuedLocal);
StartImportedQueueEntry(id);
BongoMaildropSubmissionClear(&submission);
return 0;
fail:
if (data != NULL) fclose(data);
if (control != NULL) fclose(control);
unlink(data_work);
unlink(control_work);
BongoMaildropSubmissionClear(&submission);
errno = saved_error != 0 ? saved_error : EIO;
return -1;
}
static void
CheckMaildrop(void *unused)
{
UNUSED_PARAMETER(unused);
XplRenameThread(XplGetThreadID(), "Local-maildrop Monitor");
while (BongoAgentGetState(&Agent.agent) <
BONGO_AGENT_STATE_STOPPING) {
DIR *directory = opendir(XPL_DEFAULT_MAILDROP_DIR);
struct dirent *entry;
if (directory == NULL) {
Log(LOG_ERROR, "Cannot open local maildrop %s: %s",
XPL_DEFAULT_MAILDROP_DIR, strerror(errno));
XplDelay(5000);
continue;
}
while ((entry = readdir(directory)) != NULL) {
char source[XPL_MAX_PATH + 1];
char pickup[XPL_MAX_PATH + 1];
const char *name = entry->d_name;
int result;
if (strncmp(name, "drop-", 5U) == 0) {
if (!QueueFormat(source, "%s/%s",
XPL_DEFAULT_MAILDROP_DIR, name) ||
!QueueFormat(pickup, "%s/.pickup-%s",
XPL_DEFAULT_MAILDROP_DIR, name + 5U))
continue;
if (rename(source, pickup) != 0) {
if (errno != ENOENT)
Log(LOG_WARNING,
"Cannot claim local maildrop %s: %s",
source, strerror(errno));
continue;
}
} else if (strncmp(name, ".pickup-", 8U) != 0) {
continue;
} else if (!QueueFormat(pickup, "%s/%s",
XPL_DEFAULT_MAILDROP_DIR, name)) {
continue;
}
result = ImportMaildropFile(pickup);
if (result == 0) {
if (unlink(pickup) != 0 && errno != ENOENT)
Log(LOG_WARNING, "Cannot remove local maildrop %s: %s",
pickup, strerror(errno));
} else if (result > 0) {
Log(LOG_ERROR, "Discarding invalid local maildrop %s",
pickup);
unlink(pickup);
} else {
Log(LOG_WARNING,
"Deferring local maildrop %s: %s",
pickup, strerror(errno));
}
}
closedir(directory);
XplDelay(1000);
}
}
#define MAX_CHARS_IN_PDBSEARCH 512
@@ -2956,6 +3171,8 @@ CreateQueueThreads(BOOL failed)
if (!Conf.debug) {
XplBeginCountedThread(&id, CheckQueue, STACKSPACE_Q, NULL, i, Agent.activeThreads);
}
XplBeginCountedThread(&id, CheckMaildrop, STACKSPACE_Q, NULL, i,
Agent.activeThreads);
return(0);
}
+31 -8
View File
@@ -1,13 +1,36 @@
configure_file(bongo-sendmail.py.in bongo-sendmail @ONLY)
StrictCompile()
install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bongo-sendmail
add_executable(bongo-sendmail sendmail.c)
target_compile_definitions(bongo-sendmail PRIVATE
BONGO_POSTDROP_PATH="${SBIN_INSTALL_DIR}/bongopostdrop")
target_link_libraries(bongo-sendmail PRIVATE bongoutil GLib2::GLib2)
add_executable(bongopostdrop postdrop.c)
target_compile_definitions(bongopostdrop PRIVATE
BONGO_MAILDROP_DIR="${XPL_DEFAULT_MAILDROP_DIR}")
target_link_libraries(bongopostdrop PRIVATE bongoutil GLib2::GLib2)
install(TARGETS bongo-sendmail bongopostdrop
DESTINATION ${SBIN_INSTALL_DIR})
if(BUILD_TESTING)
add_test(NAME sendmail-local-submission
COMMAND ${CMAKE_COMMAND} -E env
"PYTHONPATH=${CMAKE_SOURCE_DIR}/src/libs/python"
"PYTHONPYCACHEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/pycache"
${Python3_EXECUTABLE} -m unittest discover
-s ${CMAKE_CURRENT_SOURCE_DIR}/tests -v)
set(SENDMAIL_TEST_MAILDROP
"${CMAKE_CURRENT_BINARY_DIR}/maildrop-test")
add_executable(bongopostdrop-test-helper postdrop.c)
target_compile_definitions(bongopostdrop-test-helper PRIVATE
BONGO_MAILDROP_DIR="${SENDMAIL_TEST_MAILDROP}")
target_link_libraries(bongopostdrop-test-helper PRIVATE
bongoutil GLib2::GLib2)
add_executable(bongo-sendmail-test-bin sendmail.c)
target_compile_definitions(bongo-sendmail-test-bin PRIVATE
BONGO_POSTDROP_PATH="${CMAKE_CURRENT_BINARY_DIR}/bongopostdrop-test-helper")
target_link_libraries(bongo-sendmail-test-bin PRIVATE
bongoutil GLib2::GLib2)
add_test(NAME sendmail-c-local-submission
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/sendmail-c-test.py
$<TARGET_FILE:bongo-sendmail-test-bin>
${SENDMAIL_TEST_MAILDROP})
endif()
-33
View File
@@ -1,33 +0,0 @@
#!/usr/bin/env python3
# /****************************************************************************
# * <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.
# *
# * To contact Novell about this file by physical or electronic mail, you
# * may find current contact information at www.novell.com.
# * </Novell-copyright>
# ****************************************************************************/
import sys
sys.path.insert(0, "@PYTHON_SITEPACKAGES_PATH@")
sys.path.insert(0, "@PYTHON_SITELIB_PATH@")
from bongo.sendmail import main
if __name__ == "__main__":
raise SystemExit(main(config_dir="@XPL_DEFAULT_CONFIG_DIR@"))
+162
View File
@@ -0,0 +1,162 @@
/****************************************************************************
* <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 <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef BONGO_MAILDROP_DIR
#define BONGO_MAILDROP_DIR "/var/lib/bongo/maildrop"
#endif
static int MaildropDirectory = -1;
static char TemporaryName[96];
static void
Cleanup(int signal_number)
{
if (MaildropDirectory >= 0 && TemporaryName[0] != '\0')
unlinkat(MaildropDirectory, TemporaryName, 0);
if (signal_number != 0) _exit(128 + signal_number);
}
static int
InstallSignalHandlers(void)
{
struct sigaction action;
int signals[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM };
unsigned int index;
memset(&action, 0, sizeof(action));
action.sa_handler = Cleanup;
sigemptyset(&action.sa_mask);
for (index = 0U; index < sizeof(signals) / sizeof(signals[0]); index++) {
if (sigaction(signals[index], &action, NULL) != 0) return -1;
}
return 0;
}
static int
RandomBytes(unsigned char *data, size_t length)
{
size_t offset = 0U;
while (offset < length) {
ssize_t count = getrandom(data + offset, length - offset, 0);
if (count > 0) {
offset += (size_t)count;
continue;
}
if (count < 0 && errno == EINTR) continue;
return -1;
}
return 0;
}
static int
MakeNames(char *final_name, size_t final_size)
{
unsigned char random[16];
char encoded[sizeof(random) * 2U + 1U];
unsigned int index;
int length;
if (RandomBytes(random, sizeof(random)) != 0) return -1;
for (index = 0U; index < sizeof(random); index++)
snprintf(encoded + index * 2U, 3U, "%02x", random[index]);
length = snprintf(TemporaryName, sizeof(TemporaryName), ".tmp-%s-%ld",
encoded, (long)getpid());
if (length < 0 || (size_t)length >= sizeof(TemporaryName)) {
errno = ENAMETOOLONG;
return -1;
}
length = snprintf(final_name, final_size, "drop-%s", encoded);
if (length < 0 || (size_t)length >= final_size) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
int
main(void)
{
BongoMaildropSubmission submission = { 0 };
char final_name[64];
FILE *output = NULL;
int output_fd = -1;
int saved_error;
umask(0027);
if (InstallSignalHandlers() != 0 ||
BongoMaildropRead(stdin, BONGO_MAILDROP_MESSAGE_LIMIT,
BONGO_MAILDROP_RECIPIENT_LIMIT,
&submission) != 0) {
fprintf(stderr, "bongopostdrop: invalid submission: %s\n",
strerror(errno));
return 64;
}
submission.uid = getuid();
MaildropDirectory =
open(BONGO_MAILDROP_DIR, O_PATH | O_DIRECTORY | O_CLOEXEC);
if (MaildropDirectory < 0 || MakeNames(final_name, sizeof(final_name)) != 0)
goto fail;
output_fd = openat(MaildropDirectory, TemporaryName,
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC,
0640);
if (output_fd < 0 || fchmod(output_fd, 0640) != 0) goto fail;
output = fdopen(output_fd, "wb");
if (output == NULL) goto fail;
output_fd = -1;
if (BongoMaildropWrite(output, &submission) != 0 ||
fflush(output) != 0 || fsync(fileno(output)) != 0 ||
fclose(output) != 0) {
output = NULL;
goto fail;
}
output = NULL;
if (renameat(MaildropDirectory, TemporaryName,
MaildropDirectory, final_name) != 0)
goto fail;
TemporaryName[0] = '\0';
close(MaildropDirectory);
MaildropDirectory = -1;
BongoMaildropSubmissionClear(&submission);
return 0;
fail:
saved_error = errno != 0 ? errno : EIO;
if (output != NULL) fclose(output);
if (output_fd >= 0) close(output_fd);
Cleanup(0);
if (MaildropDirectory >= 0) close(MaildropDirectory);
BongoMaildropSubmissionClear(&submission);
fprintf(stderr, "bongopostdrop: cannot commit local message: %s\n",
strerror(saved_error));
return 75;
}
+478 -321
View File
@@ -1,355 +1,512 @@
/****************************************************************************
* <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.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
/* This is an implementation of sendmail designed to eventually conform to LSB:
* http://refspecs.freestandards.org/LSB_2.1.0/LSB-Core-generic/LSB-Core-generic/baselib-sendmail-1.html
* Other platforms may or may not find it useful.
*/
#include <bongomaildrop.h>
#include <config.h>
#include <xpl.h>
#include <ctype.h>
#include <errno.h>
#include <bongoutil.h>
#include <nmap.h>
#include <nmlib.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>
#include <msgapi.h>
#include <pwd.h>
#include "sendmail.h"
static char*
LookupUser(uid_t uid)
{
char *ret;
char *buf;
int buflen;
struct passwd pwbuf;
struct passwd *pw;
buflen = sysconf(_SC_GETPW_R_SIZE_MAX);
buf = MemMalloc(buflen + 1);
#if defined(__SVR4) && defined(__sun) && !defined(_POSIX_PTHREAD_SEMANTICS)
pw = getpwuid_r(uid, &pwbuf, buf, buflen);
#else
getpwuid_r(uid, &pwbuf, buf, buflen, &pw);
#ifndef BONGO_POSTDROP_PATH
#define BONGO_POSTDROP_PATH "/usr/sbin/bongopostdrop"
#endif
if (!pw) {
MemFree(buf);
return NULL;
}
ret = MemStrdup(pw->pw_name);
MemFree(buf);
return ret;
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);
}
int
main (int argc, char *argv[])
static int
AddRecipient(GPtrArray *recipients, const char *value)
{
Connection *nmap;
ssize_t size = 0;
char *line = NULL;
char *response;
char command[200];
int i;
ConnStartup((15*60));
MsgInit();
NMAPInitialize();
CONN_TRACE_INIT(XPL_DEFAULT_WORK_DIR, "sendmail");
// CONN_TRACE_SET_FLAGS(CONN_TRACE_ALL); /* uncomment this line and pass '--enable-conntrace' to autogen to get the agent to trace all connections */
char *copy;
unsigned int index;
/* TODO: we only allow localhost at the moment, because that's where we're
* getting our credentials. In the future, being able to remove the MDB
* stuff and connect to an arbitrary NMAP server would be nice.
*/
nmap = NMAPConnectQueue("127.0.0.1", NULL);
if (nmap == NULL) {
FatalError(1, "cannot connect to NMAP server.");
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';
}
response = MemMalloc(sizeof(char) * 1000);
if (! NMAPAuthenticateToQueue(nmap, response, 1000)) {
FatalError(1, "cannot authenticate with NMAP.");
}
MemFree(response);
/* we're now all setup, and can start doing things */
ParseArgs(argc, argv);
/* start sending the email */
NMAPSimple(nmap, "QCREA");
/* send the mail body */
NMAPSendCommand(nmap, "QSTOR MESSAGE\r\n", 15);
line = MemMalloc(sizeof(char) * 1000);
while ((size = GetLine(line, sizeof(char) * 1000, stdin)) && (size != -1)) {
if ((line[0] == '.') && (size == 3)) {
if (BongoSendmailConfig.inputmode == UNTIL_DOT) {
/* this means end of mail */
break;
} else {
/* this is mail content we need to pad to not confuse NMAP */
NMAPSendCommand(nmap, "..\r\n",
sizeof("..\r\n") - 1U);
}
} else {
/* pass the contents through to NMAP; this is the mail */
char *text = line;
if (! strncmp(line, "From:", 5) && BongoSendmailConfig.from_header) {
NMAPSendCommand(nmap, "From: ", 6);
text = BongoSendmailConfig.from_header;
} else if ((! strncmp(line, "To:", 3)) || (! strncmp(line, "Cc:", 3))) {
if (BongoSendmailConfig.extract) {
AddAddressees(line);
}
} else if (! strncmp(line, "Bcc:", 4)) {
if (BongoSendmailConfig.extract) {
AddAddressees(line);
}
/* TODO: should only remove Bcc: when To: or Cc: is present, RFC 2822 */
text = NULL;
}
if (text) {
NMAPSendCommand(nmap, text, strlen(text));
}
}
}
NMAPSimple(nmap, ".");
MemFree(line);
/* send the mail envelope */
for (i = 0; i < BongoSendmailConfig.addressees; i++) {
char *address = BongoSendmailConfig.deliver_to[i];
sprintf(command, "QSTOR TO %s %s 2", address, address);
NMAPSimple(nmap, command);
}
if (! BongoSendmailConfig.from_envelope) {
char *host;
char *username = getenv("USER");
if (! username) {
username = getenv("LOGNAME");
}
if (! username) {
username = LookupUser(getuid());
}
host = MemMalloc(256);
gethostname(host, 256);
BongoSendmailConfig.from_envelope = MemMalloc(1000);
snprintf(BongoSendmailConfig.from_envelope, 1000,"%s@%s", username, host);
MemFree(host);
}
sprintf(command, "QSTOR FROM %s -", BongoSendmailConfig.from_envelope);
NMAPSimple(nmap, command);
NMAPSimple(nmap, "QRUN");
NMAPQuit(nmap);
CONN_TRACE_SHUTDOWN();
return(0);
}
void
ParseArgs (int argc, char *argv[])
{
int arg = 1;
int i;
BongoSendmailConfig.inputmode = UNTIL_DOT;
BongoSendmailConfig.extract = FALSE;
while ((arg < argc) && (argv[arg][0] == '-')) {
switch(argv[arg][1]) {
case 'b':
switch (argv[arg][2]) {
case 'p':
FatalError(50, "listing mail queue jobs not supported.");
PrintUsage();
break;
case 's':
FatalError(50, "running in smtpd mode is not supported.");
PrintUsage();
break;
case 'm':
BongoSendmailConfig.inputmode = UNTIL_DOT;
break;
default:
XplConsolePrintf("Unknown argument: %s\n", argv[arg]);
break;
}
break;
case 'i':
BongoSendmailConfig.inputmode = UNTIL_EOF;
break;
case 't':
/* TODO: we don't remove addresses passed in argv when in -t i
* mode, which we should for LSB compliance. Horrible
* behaviour tho'.
*/
BongoSendmailConfig.extract = TRUE;
break;
case 'F':
arg++;
BongoSendmailConfig.from_header = argv[arg];
break;
case 'f':
case 'r': /* postfix sendmail accepts -r ... */
arg++;
BongoSendmailConfig.from_envelope = argv[arg];
break;
case 'e':
/* ignore these arguments */
break;
case 'o':
if(argv[arg][2] == 'i') {
BongoSendmailConfig.inputmode = UNTIL_EOF;
}
/* ignore other -o arguments */
break;
default:
XplConsolePrintf("Unknown argument: %s\n", argv[arg]);
break;
}
arg++;
}
for (i = 0; i < (argc - arg); i++) {
AddAddressee(argv[arg]);
}
}
void
AddAddressees (char *addressees)
{
/* This is supposed to extract individual addresses from a header. It has
* to deal with lines such as (from RFC 2822):
* To: Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>
* Cc: <boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>
* In this simple scenario, it's just pulling out things which look like
* actual addresses.
*/
int start, i;
int has_at = 0;
int addressees_length;
const char *invalid_email_chars = "<>, \t\r\n";
char *email;
addressees_length = strlen(addressees);
start = -1;
for (i = 0; i < addressees_length; i++) {
int is_invalid_mail_char = 0;
unsigned int j;
for (j = 0; j < strlen(invalid_email_chars); j++) {
if (addressees[i] == invalid_email_chars[j]) {
is_invalid_mail_char = 1;
}
}
if (is_invalid_mail_char || (i == addressees_length)) {
if ((start != -1) && has_at && ((i - start)<1000)) {
email = MemMalloc ((sizeof(char) * (i - start)) + 1);
strncpy (email, &addressees[start], i - start);
email[i - start] = 0;
AddAddressee(email);
}
start = -1;
has_at = 0;
} else if (addressees[i] == '@') {
has_at = 1;
} else {
if (start == -1) start = i;
}
}
}
void
AddAddressee (char *addressee)
{
/* this is someone we want to send the mail to (ie., address envelope to) */
int size;
BongoSendmailConfig.addressees++;
size = (sizeof(char *) * BongoSendmailConfig.addressees);
if (BongoSendmailConfig.deliver_to) {
BongoSendmailConfig.deliver_to = MemRealloc(BongoSendmailConfig.deliver_to, size);
} else {
BongoSendmailConfig.deliver_to = MemMalloc(size);
}
BongoSendmailConfig.deliver_to[BongoSendmailConfig.addressees - 1] = addressee;
}
void
FatalError (const int exit_code, const char *message)
{
fprintf(stderr, "bongosendmail: %s\n", message);
exit(exit_code);
}
void
PrintUsage ()
{
XplConsolePrintf("bongosendmail: fatal: usage: [options] [addresses]\n");
exit(0);
}
int
GetLine (char *buf, const unsigned int bufsize, FILE *fh)
{
/* read a line from the file handle, make sure it's \r\n terminated */
if (fgets (buf, bufsize - 1, fh ) == NULL) {
if (!BongoMaildropAddressValid(copy)) {
g_free(copy);
return -1;
} else {
unsigned int strsize = strlen(buf);
buf[strsize-1] = '\r';
buf[strsize] = '\n';
buf[strsize+1] = 0;
return strsize + 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;
}
void
NMAPSimple (Connection *nmap, char *command)
static const char *
OptionValue(int argc, char **argv, int *index)
{
/* send NMAP a command and burp if it doesn't succeed */
int repcode;
char response[1024];
NMAPSendCommand(nmap, command, strlen(command));
NMAPSendCommand(nmap, "\r\n", 2);
repcode = NMAPReadAnswer(nmap, response, sizeof(response), FALSE);
if (repcode != 1000) {
XplConsolePrintf("ERROR: Sent command %s\n", command);
XplConsolePrintf("ERROR: NMAP server returned %d: %s\n", repcode, response);
exit(-2);
}
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;
}
-49
View File
@@ -1,49 +0,0 @@
/****************************************************************************
* <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.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#ifndef _BONGO_SENDMAIL
#define _BONGO_SENDMAIL
enum InputMode {
UNTIL_DOT, UNTIL_EOF,
};
struct config {
char *from_header;
char *from_envelope;
enum InputMode inputmode;
int extract;
int addressees;
char **deliver_to;
} BongoSendmailConfig;
/* bongosendmail.c */
void FatalError(const int error_code, const char *string);
void PrintUsage(void);
void NMAPSimple(Connection *nmap, char *command);
void ParseArgs(int argc, char *argv[]);
int GetLine(char *buffer, const unsigned int bufsize, FILE *handle);
void AddAddressees (char *addressees);
void AddAddressee (char *addressee);
#endif /* _BONGO_SENDMAIL */
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# /****************************************************************************
# * <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.
# * </Novell-copyright>
# ****************************************************************************/
"""Black-box checks for the C sendmail/postdrop privilege boundary."""
from __future__ import annotations
import shutil
import struct
import subprocess
import sys
from pathlib import Path
def decode(path: Path) -> tuple[int, str, list[str], bytes]:
data = memoryview(path.read_bytes())
offset = 0
def take(length: int) -> bytes:
nonlocal offset
if offset + length > len(data):
raise AssertionError("truncated maildrop")
value = bytes(data[offset:offset + length])
offset += length
return value
def u32() -> int:
return struct.unpack(">I", take(4))[0]
def u64() -> int:
return struct.unpack(">Q", take(8))[0]
assert take(8) == b"BONGOMD1"
uid = u64()
sender_length = u32()
recipient_count = u32()
message_length = u64()
sender = take(sender_length).decode("utf-8")
recipients = [
take(u32()).decode("utf-8") for _ in range(recipient_count)
]
message = take(message_length)
assert offset == len(data)
return uid, sender, recipients, message
def main() -> int:
if len(sys.argv) != 3:
raise SystemExit("usage: sendmail-c-test.py SENDMAIL MAILDROP")
sendmail = Path(sys.argv[1])
maildrop = Path(sys.argv[2])
shutil.rmtree(maildrop, ignore_errors=True)
maildrop.mkdir(mode=0o700)
raw = (
b"From: Sender <sender@example.test>\r\n"
b"To: One <one@example.test>\r\n"
b"Cc: two@example.test\r\n"
b"Bcc: Hidden <hidden@example.test>\r\n"
b"\t, folded@example.test\r\n"
b"Subject: C frontend\r\n"
b"\r\n"
b"one\r\n"
b"..two\r\n"
b".\r\n"
b"ignored\r\n"
)
completed = subprocess.run(
[
str(sendmail), "-t", "-f", "sender@example.test",
"explicit@example.test",
],
input=raw,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
assert completed.returncode == 0, completed.stderr.decode()
drops = list(maildrop.glob("drop-*"))
assert len(drops) == 1
_, sender, recipients, message = decode(drops[0])
assert sender == "sender@example.test"
assert recipients == [
"explicit@example.test",
"one@example.test",
"two@example.test",
"hidden@example.test",
"folded@example.test",
]
assert b"\r\nBcc:" not in b"\r\n" + message
assert b"\r\n\t, folded@example.test" not in message
assert message.endswith(b"one\r\n.two\r\n")
shutil.rmtree(maildrop)
return 0
if __name__ == "__main__":
raise SystemExit(main())
-125
View File
@@ -1,125 +0,0 @@
# /****************************************************************************
# * <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.
# *
# * To contact Novell about this file by physical or electronic mail, you
# * may find current contact information at www.novell.com.
# * </Novell-copyright>
# ****************************************************************************/
import io
import json
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from bongo import sendmail
class SendmailTest(unittest.TestCase):
def test_parses_combined_options_and_attached_sender(self):
options = sendmail.parse_options(
["-oi", "-t", "-froot@example.test", "extra@example.test"])
self.assertTrue(options.ignore_dots)
self.assertTrue(options.extract_recipients)
self.assertEqual(options.envelope_from, "root@example.test")
self.assertEqual(options.recipients, ("extra@example.test",))
def test_dot_terminated_input_is_unstuffed(self):
source = io.BytesIO(b"Subject: test\r\n\r\none\r\n..two\r\n.\r\nignored\r\n")
self.assertEqual(
sendmail.read_message(source, False),
b"Subject: test\r\n\r\none\r\n.two\r\n")
def test_extracts_all_recipients_and_removes_bcc(self):
options = sendmail.parse_options(["-t", "extra@example.test"])
raw = (
b"From: sender@example.test\r\n"
b"To: One <one@example.test>\r\n"
b"Cc: two@example.test\r\n"
b"Bcc: Hidden <hidden@example.test>\r\n"
b"\r\nbody\r\n"
)
rendered, recipients = sendmail.prepare_message(raw, options)
self.assertEqual(
recipients,
["extra@example.test", "one@example.test", "two@example.test",
"hidden@example.test"])
self.assertNotIn(b"Bcc:", rendered)
self.assertIn(b"body", rendered)
def test_prefers_enabled_internal_relay(self):
with tempfile.TemporaryDirectory() as directory:
Path(directory, "smtp").write_text(json.dumps({
"internal_relay_enabled": True,
"internal_relay_bind_address": "0.0.0.0",
"internal_relay_port": 2026,
"port": 2025,
}))
self.assertEqual(
sendmail.select_endpoint(directory),
sendmail.SubmissionEndpoint("127.0.0.1", 2026))
def test_falls_back_to_normal_local_smtp(self):
with tempfile.TemporaryDirectory() as directory:
Path(directory, "smtp").write_text(json.dumps({
"internal_relay_enabled": False,
"port": 2525,
}))
self.assertEqual(
sendmail.select_endpoint(directory),
sendmail.SubmissionEndpoint("127.0.0.1", 2525))
@mock.patch("bongo.sendmail.smtplib.SMTP")
def test_submission_upgrades_to_starttls(self, smtp_class):
client = smtp_class.return_value.__enter__.return_value
client.has_extn.return_value = True
client.sendmail.return_value = {}
endpoint = sendmail.SubmissionEndpoint("127.0.0.1", 26)
sendmail.submit_message(
endpoint, "sender@example.test", ["to@example.test"],
b"Subject: test\r\n\r\nbody\r\n", 5)
smtp_class.assert_called_once_with("127.0.0.1", 26, timeout=5)
client.starttls.assert_called_once()
client.sendmail.assert_called_once_with(
"sender@example.test", ["to@example.test"],
b"Subject: test\r\n\r\nbody\r\n")
@mock.patch("bongo.sendmail.submit_message")
def test_main_submits_without_system_credentials(self, submit):
with tempfile.TemporaryDirectory() as directory:
Path(directory, "smtp").write_text(json.dumps({
"internal_relay_enabled": True,
"internal_relay_bind_address": "127.0.0.1",
"internal_relay_port": 26,
}))
result = sendmail.main(
["-oi", "-t", "-f", "sender@example.test"],
config_dir=directory,
stdin=io.BytesIO(
b"To: receiver@example.test\r\nSubject: test\r\n\r\nbody\r\n"),
stderr=io.StringIO())
self.assertEqual(result, sendmail.EX_OK)
submit.assert_called_once()
self.assertEqual(submit.call_args.args[0].port, 26)
self.assertEqual(submit.call_args.args[1], "sender@example.test")
self.assertEqual(submit.call_args.args[2], ["receiver@example.test"])
if __name__ == "__main__":
unittest.main()
-267
View File
@@ -1,267 +0,0 @@
# /****************************************************************************
# * <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.
# *
# * To contact Novell about this file by physical or electronic mail, you
# * may find current contact information at www.novell.com.
# * </Novell-copyright>
# ****************************************************************************/
"""Restricted sendmail-compatible local submission frontend.
This frontend deliberately does not read Bongo's system credential. It
submits through the rate-limited internal SMTP listener, or through the normal
local SMTP listener when the internal listener is disabled.
"""
from __future__ import annotations
import email.policy
import getpass
import json
import os
import smtplib
import socket
import ssl
import sys
from dataclasses import dataclass
from email.generator import BytesGenerator
from email.parser import BytesParser
from email.utils import getaddresses
from io import BytesIO
from pathlib import Path
from typing import BinaryIO, Sequence, TextIO
EX_OK = 0
EX_USAGE = 64
EX_UNAVAILABLE = 69
EX_TEMPFAIL = 75
class SubmissionError(RuntimeError):
"""The message could not be submitted."""
@dataclass(frozen=True)
class SendmailOptions:
envelope_from: str | None
from_name: str | None
extract_recipients: bool
ignore_dots: bool
recipients: tuple[str, ...]
@dataclass(frozen=True)
class SubmissionEndpoint:
host: str
port: int
opportunistic_tls: bool = True
def _option_value(arguments: Sequence[str], index: int, option: str) -> tuple[str, int]:
argument = arguments[index]
if len(argument) > 2:
return argument[2:], index
if index + 1 >= len(arguments):
raise SubmissionError(f"{option} requires an argument")
return arguments[index + 1], index + 1
def parse_options(arguments: Sequence[str]) -> SendmailOptions:
envelope_from = None
from_name = None
extract_recipients = False
ignore_dots = False
recipients: list[str] = []
index = 0
while index < len(arguments):
argument = arguments[index]
if argument == "--":
recipients.extend(arguments[index + 1:])
break
if not argument.startswith("-") or argument == "-":
recipients.append(argument)
index += 1
continue
if argument in ("-i", "-oi"):
ignore_dots = True
elif argument == "-t":
extract_recipients = True
elif argument in ("-bm", "-om"):
pass
elif argument.startswith("-f") or argument.startswith("-r"):
envelope_from, index = _option_value(arguments, index, argument[:2])
elif argument.startswith("-F"):
from_name, index = _option_value(arguments, index, "-F")
elif argument in ("-o", "-O"):
_, index = _option_value(arguments, index, argument)
elif argument.startswith("-o") or argument.startswith("-O"):
# Common sendmail compatibility controls are advisory for this
# restricted local frontend. -oi was handled above.
pass
elif argument in ("-Am", "-Ac"):
pass
elif argument in ("-B", "-N", "-R", "-V"):
_, index = _option_value(arguments, index, argument)
elif argument in ("--help", "-h"):
raise SubmissionError("usage")
else:
raise SubmissionError(f"unsupported sendmail option {argument}")
index += 1
return SendmailOptions(
envelope_from=envelope_from,
from_name=from_name,
extract_recipients=extract_recipients,
ignore_dots=ignore_dots,
recipients=tuple(recipients),
)
def read_message(stream: BinaryIO, ignore_dots: bool) -> bytes:
if ignore_dots:
return stream.read()
lines: list[bytes] = []
for line in stream:
if line.rstrip(b"\r\n") == b".":
break
if line.startswith(b".."):
line = line[1:]
lines.append(line)
return b"".join(lines)
def prepare_message(raw: bytes, options: SendmailOptions) -> tuple[bytes, list[str]]:
try:
message = BytesParser(policy=email.policy.SMTP).parsebytes(raw)
except (TypeError, ValueError) as error:
raise SubmissionError(f"cannot parse message: {error}") from error
recipients = list(options.recipients)
if options.extract_recipients:
headers: list[str] = []
for name in ("to", "cc", "bcc"):
headers.extend(str(value) for value in message.get_all(name, []))
recipients.extend(address for _, address in getaddresses(headers) if address)
while "bcc" in message:
del message["bcc"]
if options.from_name and "from" not in message:
username = getpass.getuser()
message["From"] = f"{options.from_name} <{username}@{socket.getfqdn()}>"
recipients = list(dict.fromkeys(address.strip() for address in recipients
if address.strip()))
if not recipients:
raise SubmissionError("no recipients were specified")
output = BytesIO()
BytesGenerator(output, policy=email.policy.SMTP).flatten(message)
return output.getvalue(), recipients
def default_envelope_sender(options: SendmailOptions) -> str:
if options.envelope_from is not None:
return options.envelope_from.strip("<>")
return f"{getpass.getuser()}@{socket.getfqdn()}"
def _read_configuration(config_dir: str | os.PathLike[str], name: str) -> dict:
path = Path(config_dir) / name
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as error:
raise SubmissionError(f"cannot read Bongo {name} configuration: {error}") from error
if not isinstance(value, dict):
raise SubmissionError(f"Bongo {name} configuration is not an object")
return value
def select_endpoint(config_dir: str | os.PathLike[str]) -> SubmissionEndpoint:
override_host = os.environ.get("BONGO_SENDMAIL_HOST")
override_port = os.environ.get("BONGO_SENDMAIL_PORT")
if override_host or override_port:
try:
port = int(override_port or "25")
except ValueError as error:
raise SubmissionError("BONGO_SENDMAIL_PORT is not an integer") from error
return SubmissionEndpoint(override_host or "127.0.0.1", port)
smtp = _read_configuration(config_dir, "smtp")
if smtp.get("internal_relay_enabled"):
host = str(smtp.get("internal_relay_bind_address") or "127.0.0.1")
if host in ("0.0.0.0", "::", "[::]"):
host = "127.0.0.1"
return SubmissionEndpoint(host, int(smtp.get("internal_relay_port", 26)))
return SubmissionEndpoint("127.0.0.1", int(smtp.get("port", 25)))
def submit_message(endpoint: SubmissionEndpoint, envelope_from: str,
recipients: Sequence[str], message: bytes,
timeout: float = 30.0) -> None:
try:
with smtplib.SMTP(endpoint.host, endpoint.port, timeout=timeout) as client:
client.ehlo_or_helo_if_needed()
if endpoint.opportunistic_tls and client.has_extn("starttls"):
# This is a same-host submission hop. Public PKI identity is
# verified on Bongo's external SMTP delivery path.
context = ssl._create_unverified_context()
client.starttls(context=context)
client.ehlo()
refused = client.sendmail(envelope_from, list(recipients), message)
if refused:
details = ", ".join(sorted(refused))
raise SubmissionError(f"recipients refused: {details}")
except SubmissionError:
raise
except (OSError, smtplib.SMTPException) as error:
raise SubmissionError(f"local SMTP submission failed: {error}") from error
def _usage(program: str, stream: TextIO) -> None:
stream.write(
f"Usage: {program} [-i] [-t] [-f sender] [-F name] [recipient ...]\n"
)
def main(arguments: Sequence[str] | None = None, *,
config_dir: str | os.PathLike[str] = "/etc/bongo/config.d",
stdin: BinaryIO | None = None, stderr: TextIO | None = None) -> int:
arguments = list(sys.argv[1:] if arguments is None else arguments)
stdin = sys.stdin.buffer if stdin is None else stdin
stderr = sys.stderr if stderr is None else stderr
try:
options = parse_options(arguments)
raw = read_message(stdin, options.ignore_dots)
message, recipients = prepare_message(raw, options)
endpoint = select_endpoint(config_dir)
timeout = float(os.environ.get("BONGO_SENDMAIL_TIMEOUT", "30"))
submit_message(endpoint, default_envelope_sender(options), recipients,
message, timeout)
return EX_OK
except SubmissionError as error:
if str(error) == "usage":
_usage(Path(sys.argv[0]).name, stderr)
return EX_OK
stderr.write(f"bongo-sendmail: {error}\n")
return EX_USAGE if "option" in str(error) or "recipients" in str(error) \
else EX_TEMPFAIL
except (ValueError, OverflowError) as error:
stderr.write(f"bongo-sendmail: invalid configuration: {error}\n")
return EX_UNAVAILABLE
+5
View File
@@ -12,6 +12,7 @@ add_library(bongoutil SHARED
quicksort.c
utf7mod.c
logging.c
maildrop.c
domain.c
bongoutil.c)
@@ -37,4 +38,8 @@ if(BUILD_TESTING)
add_executable(util-domain-test tests/domain-test.c domain.c)
target_link_libraries(util-domain-test PRIVATE LibIDN2::LibIDN2)
add_test(NAME util-domain COMMAND util-domain-test)
add_executable(util-maildrop-test tests/maildrop-test.c maildrop.c)
target_link_libraries(util-maildrop-test PRIVATE GLib2::GLib2)
add_test(NAME util-maildrop COMMAND util-maildrop-test)
endif()
+257
View File
@@ -0,0 +1,257 @@
/****************************************************************************
* <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 <errno.h>
#include <limits.h>
#include <string.h>
#define MAILDROP_MAGIC "BONGOMD1"
#define MAILDROP_MAGIC_LENGTH 8U
static int
WriteBytes(FILE *stream, const void *value, size_t length)
{
if (length != 0U && fwrite(value, 1U, length, stream) != length) {
if (errno == 0) errno = EIO;
return -1;
}
return 0;
}
static int
ReadBytes(FILE *stream, void *value, size_t length)
{
if (length != 0U && fread(value, 1U, length, stream) != length) {
errno = ferror(stream) ? (errno != 0 ? errno : EIO) : EINVAL;
return -1;
}
return 0;
}
static int
WriteU32(FILE *stream, uint32_t value)
{
unsigned char data[4];
data[0] = (unsigned char)(value >> 24);
data[1] = (unsigned char)(value >> 16);
data[2] = (unsigned char)(value >> 8);
data[3] = (unsigned char)value;
return WriteBytes(stream, data, sizeof(data));
}
static int
ReadU32(FILE *stream, uint32_t *value)
{
unsigned char data[4];
if (ReadBytes(stream, data, sizeof(data)) != 0) return -1;
*value = ((uint32_t)data[0] << 24) |
((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) |
(uint32_t)data[3];
return 0;
}
static int
WriteU64(FILE *stream, uint64_t value)
{
unsigned char data[8];
unsigned int index;
for (index = 0U; index < sizeof(data); index++)
data[index] = (unsigned char)(value >> ((7U - index) * 8U));
return WriteBytes(stream, data, sizeof(data));
}
static int
ReadU64(FILE *stream, uint64_t *value)
{
unsigned char data[8];
unsigned int index;
if (ReadBytes(stream, data, sizeof(data)) != 0) return -1;
*value = 0U;
for (index = 0U; index < sizeof(data); index++)
*value = (*value << 8) | data[index];
return 0;
}
int
BongoMaildropAddressValid(const char *address)
{
const unsigned char *cursor = (const unsigned char *)address;
size_t length;
if (address == NULL) return 0;
length = strlen(address);
if (length == 0U || length > BONGO_MAILDROP_ADDRESS_LIMIT) return 0;
while (*cursor != '\0') {
if (*cursor == '\r' || *cursor == '\n' || *cursor == '\0' ||
*cursor < 0x20U || *cursor == 0x7fU)
return 0;
cursor++;
}
return 1;
}
int
BongoMaildropWrite(FILE *stream, const BongoMaildropSubmission *submission)
{
unsigned int index;
size_t sender_length;
if (stream == NULL || submission == NULL ||
(submission->sender == NULL ||
(submission->sender[0] != '\0' &&
!BongoMaildropAddressValid(submission->sender))) ||
submission->recipients == NULL ||
submission->recipients->len == 0U ||
submission->recipients->len > BONGO_MAILDROP_RECIPIENT_LIMIT ||
submission->message == NULL || submission->message_length == 0U ||
submission->message_length > BONGO_MAILDROP_MESSAGE_LIMIT) {
errno = EINVAL;
return -1;
}
sender_length = strlen(submission->sender);
for (index = 0U; index < submission->recipients->len; index++) {
if (!BongoMaildropAddressValid(
g_ptr_array_index(submission->recipients, index))) {
errno = EINVAL;
return -1;
}
}
if (WriteBytes(stream, MAILDROP_MAGIC, MAILDROP_MAGIC_LENGTH) != 0 ||
WriteU64(stream, (uint64_t)submission->uid) != 0 ||
WriteU32(stream, (uint32_t)sender_length) != 0 ||
WriteU32(stream, submission->recipients->len) != 0 ||
WriteU64(stream, submission->message_length) != 0 ||
WriteBytes(stream, submission->sender, sender_length) != 0)
return -1;
for (index = 0U; index < submission->recipients->len; index++) {
const char *recipient =
g_ptr_array_index(submission->recipients, index);
size_t length = strlen(recipient);
if (WriteU32(stream, (uint32_t)length) != 0 ||
WriteBytes(stream, recipient, length) != 0)
return -1;
}
return WriteBytes(stream, submission->message,
submission->message_length);
}
int
BongoMaildropRead(FILE *stream, size_t message_limit,
unsigned int recipient_limit,
BongoMaildropSubmission *submission)
{
unsigned char magic[MAILDROP_MAGIC_LENGTH];
uint64_t uid;
uint64_t message_length;
uint32_t sender_length;
uint32_t recipient_count;
uint32_t length;
uint32_t index;
int trailing;
if (stream == NULL || submission == NULL || message_limit == 0U ||
recipient_limit == 0U) {
errno = EINVAL;
return -1;
}
memset(submission, 0, sizeof(*submission));
if (ReadBytes(stream, magic, sizeof(magic)) != 0 ||
memcmp(magic, MAILDROP_MAGIC, sizeof(magic)) != 0 ||
ReadU64(stream, &uid) != 0 ||
ReadU32(stream, &sender_length) != 0 ||
ReadU32(stream, &recipient_count) != 0 ||
ReadU64(stream, &message_length) != 0 ||
sender_length > BONGO_MAILDROP_ADDRESS_LIMIT ||
recipient_count == 0U || recipient_count > recipient_limit ||
recipient_count > BONGO_MAILDROP_RECIPIENT_LIMIT ||
message_length == 0U || message_length > message_limit ||
message_length > BONGO_MAILDROP_MESSAGE_LIMIT ||
message_length > SIZE_MAX) {
errno = EINVAL;
return -1;
}
submission->uid = (uid_t)uid;
if ((uint64_t)submission->uid != uid) {
errno = EINVAL;
return -1;
}
submission->sender = g_malloc((size_t)sender_length + 1U);
if (ReadBytes(stream, submission->sender, sender_length) != 0)
goto fail;
submission->sender[sender_length] = '\0';
if (submission->sender[0] != '\0' &&
!BongoMaildropAddressValid(submission->sender)) {
errno = EINVAL;
goto fail;
}
submission->recipients =
g_ptr_array_new_with_free_func(g_free);
for (index = 0U; index < recipient_count; index++) {
char *recipient;
if (ReadU32(stream, &length) != 0 || length == 0U ||
length > BONGO_MAILDROP_ADDRESS_LIMIT) {
errno = EINVAL;
goto fail;
}
recipient = g_malloc((size_t)length + 1U);
if (ReadBytes(stream, recipient, length) != 0) {
g_free(recipient);
goto fail;
}
recipient[length] = '\0';
if (!BongoMaildropAddressValid(recipient)) {
g_free(recipient);
errno = EINVAL;
goto fail;
}
g_ptr_array_add(submission->recipients, recipient);
}
submission->message = g_malloc((size_t)message_length);
submission->message_length = (size_t)message_length;
if (ReadBytes(stream, submission->message,
submission->message_length) != 0)
goto fail;
trailing = fgetc(stream);
if (trailing != EOF || ferror(stream)) {
errno = EINVAL;
goto fail;
}
return 0;
fail:
BongoMaildropSubmissionClear(submission);
return -1;
}
void
BongoMaildropSubmissionClear(BongoMaildropSubmission *submission)
{
if (submission == NULL) return;
g_free(submission->sender);
if (submission->recipients != NULL)
g_ptr_array_free(submission->recipients, TRUE);
g_free(submission->message);
memset(submission, 0, sizeof(*submission));
}
+67
View File
@@ -0,0 +1,67 @@
/****************************************************************************
* <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.
* </Novell-copyright>
****************************************************************************/
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <bongomaildrop.h>
int
main(void)
{
static const unsigned char message[] =
"Subject: local submission\r\n\r\nbody\r\n";
BongoMaildropSubmission input = { 0 };
BongoMaildropSubmission output = { 0 };
FILE *stream = tmpfile();
assert(stream != NULL);
input.uid = 1234;
input.sender = g_strdup("sender@example.test");
input.recipients = g_ptr_array_new_with_free_func(g_free);
g_ptr_array_add(input.recipients, g_strdup("one@example.test"));
g_ptr_array_add(input.recipients, g_strdup("two@example.test"));
input.message = g_memdup2(message, sizeof(message) - 1U);
input.message_length = sizeof(message) - 1U;
assert(BongoMaildropWrite(stream, &input) == 0);
assert(fflush(stream) == 0);
rewind(stream);
assert(BongoMaildropRead(stream, 1024U, 10U, &output) == 0);
assert(output.uid == input.uid);
assert(strcmp(output.sender, input.sender) == 0);
assert(output.recipients->len == 2U);
assert(strcmp(g_ptr_array_index(output.recipients, 1),
"two@example.test") == 0);
assert(output.message_length == input.message_length);
assert(memcmp(output.message, input.message, input.message_length) == 0);
BongoMaildropSubmissionClear(&output);
BongoMaildropSubmissionClear(&input);
fclose(stream);
assert(!BongoMaildropAddressValid("bad\naddress@example.test"));
assert(!BongoMaildropAddressValid(""));
stream = tmpfile();
assert(stream != NULL);
assert(fwrite("broken", 1U, 6U, stream) == 6U);
rewind(stream);
errno = 0;
assert(BongoMaildropRead(stream, 1024U, 10U, &output) == -1);
assert(errno == EINVAL);
fclose(stream);
return 0;
}