/*
* lsm - Leightweighted local sendmail replacement
*
* 2024, 2025 asdala.de
*/
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <string.h>
#include <libgen.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdarg.h>
#include <pwd.h>
#include <grp.h>
#include <sys/stat.h>
#include <sysexits.h>
#define INBOX "/var/mail/"
#define INBOX_PERMS 0600
#define FIX_INBOX_PERMS
#define ALIASES "/etc/aliases"
#define SDR "MAILER-DAEMON" /* RFC801 */
#define RCP "root"
#define ADDRSIZE 32
#define INF(...) msg(__VA_ARGS__)
#define ERR(...) msg(__VA_ARGS__)
static int ign_dot, scan_rcp, verbose;
static const char *sdr = SDR, *rcp = RCP, *prg;
static char ln[1001]; /* RFC2822 */
/** Message Handler **/
static void msg(const char *fmt, ...)
{
va_list varg;
fprintf(stderr, "lsm: ");
va_start(varg, fmt);
vfprintf(stderr, fmt, varg);
va_end(varg);
fputc('\n', stderr);
}
/** Help **/
static void help(void)
{
msg(
"lsm/0.3 - Local Sendmail Replacement\n"
"Usage: %s [-itv?] [-oi] [-bm] [-fsender] [recipient]\n"
" -bm Ignored\n"
" -i Ignore single dots in message\n"
" -oi Like -i\n"
" -t Scan message for recipient\n"
" -v Verbose\n"
" -? This help"
, prg);
}
/** ADT ab **/
static struct tab {
int alloc, len;
char *p;
} ab;
#define abinit() /* EMPTY */
/** Expand ab **/
static void abgrow(int nlen)
{
if (nlen >= ab.alloc) {
int nalloc;
for (nalloc = ab.alloc; nalloc <= nlen; nalloc += BUFSIZ)
;
char *np = realloc(ab.p, nalloc);
if (!np) {
ERR("mail too large: %m!");
exit(EX_SOFTWARE);
}
ab.p = np;
ab.alloc = nalloc;
}
}
/** Store line into ab **/
static void abputs(const char *ln)
{
int len = strlen(ln);
int nlen = ab.len + len;
abgrow(nlen);
char *p = ab.p + ab.len;
memcpy(p, ln, len + 1);
ab.len = nlen;
}
/** Store char into ab **/
static void abputc(const char c)
{
int nlen = ab.len + 1;
abgrow(nlen);
char *p = ab.p + ab.len;
*p++ = c;
*p = '\0';
ab.len = nlen;
}
/** Free ab **/
static void abdone(void)
{
free(ab.p);
}
/** Get alias for recipient **/
static int aliased(const char *rcp, char *alias)
{
#define SEP ":, \t\n"
FILE *f = fopen(ALIASES, "r");
if (f == NULL)
return 0;
int found = 0;
while (!found && fgets(ln, sizeof ln, f)) {
/* rcp found? */
char *p = strtok(ln, SEP);
if (p == NULL || (*p != '*' && strcmp(p, rcp) != 0))
continue;
/* alias for rcp found? */
p = strtok(NULL, SEP);
if (p == NULL)
continue;
if (strlen(p) >= ADDRSIZE)
break;
strcpy(alias, p);
found = 1;
}
fclose(f);
return found;
#undef SEP
}
/** Write Message into Inbox **/
static int msg_write(const char *inbox, uid_t ubx, gid_t gbx, const char *sdr)
{
FILE *f;
/* Correct inbox owner, group or permissions */
#ifdef FIX_INBOX_PERMS
struct stat stb;
if (stat(inbox, &stb) == 0) {
if ((stb.st_mode & ACCESSPERMS) != INBOX_PERMS) {
if (chmod(inbox, INBOX_PERMS)) {
ERR("Couldn't fix permissions of %s to 0%o: %m!", inbox, INBOX_PERMS);
return EX_NOPERM;
}
}
if (stb.st_uid != ubx || stb.st_gid != gbx) {
if (chown(inbox, ubx, gbx)) {
ERR("Couldn't fix owner:group of %s to %i:%i: %m!", inbox, ubx, gbx);
return EX_NOPERM;
}
}
}
#endif
/* Open inbox as user:mail */
gid_t ruid = geteuid(); uid_t rgid = getegid();
if (setegid(gbx)) {
ERR("Couldn't set process EGID to %i: %m!", gbx);
return EX_NOPERM;
}
if (seteuid(ubx)) {
ERR("Couldn't set process EUID to %i: %m!", ubx);
return EX_NOPERM;
}
/* Write message to inbox */
int err = 0;
if ((f = fopen(inbox, "a"))) {
int fd = fileno(f);
struct flock fl = { 0 };
fl.l_type = F_WRLCK;
if (fcntl(fd, F_SETLK, &fl) != -1) {
time_t t = time(NULL);
fprintf(f, "From %s %s%s", sdr, asctime(gmtime(&t)), ab.p);
if (ferror(f))
err = EX_IOERR;
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &fl);
}
else
err = EX_TEMPFAIL;
fclose(f);
}
else
err = EX_CANTCREAT;
if (err)
ERR("%s: %m!", inbox);
if (seteuid(ruid) || setegid(rgid))
ERR("Couldn't reset process IDs: %m!");
return err;
}
/** Copy Message into Inbox **/
static int msg_copy(void)
{
int body = 0;
char rcpbuf[ADDRSIZE], inbox[64] = INBOX;
struct passwd *ui;
struct group *gi;
uid_t ubx;
gid_t gbx;
while (fgets(ln, sizeof ln, stdin) && (ign_dot || *ln != '.')) {
/* Track location: head or body? */
if (*ln == '\n') {
body = 1;
scan_rcp = 0;
}
/* W/out rcp on command line and -t, scan for "To: " */
if (scan_rcp)
if (sscanf(ln, "To: %32s", rcpbuf) == 1) {
rcp = rcpbuf;
if (verbose)
INF("Using 'To: %s' for recipient.", rcp);
}
/* RFC822: Quote lines starting w/ "From " inside body */
if (strncmp(ln, "From ", 5) == 0) {
if (verbose)
INF("Quoting 'From ' line.");
if (body)
abputc('>');
else
*ln = '\0';
}
abputs(ln);
}
if (ferror(stdin)) {
ERR("stdin: %m!");
return EX_NOINPUT;
}
/* Append 2 LF for partial, 1 LF for full line */
if (ab.len && ab.p[ab.len - 1] != '\n')
abputc('\n');
abputc('\n');
/* Alias? */
if (aliased(rcp, rcpbuf)) {
rcp = rcpbuf;
if (verbose)
INF("Using alias %s.", rcp);
}
/* Get recipient UID */
errno = 0;
ui = getpwnam(rcp);
if (ui == NULL) {
ERR((errno ? "%s: %m!" : "%s: Unknown recipient!"), rcp);
return EX_NOUSER;
}
ubx = ui->pw_uid;
/* Get mail GID */
errno = 0;
gi = getgrnam("mail");
if (gi == NULL) {
ERR(errno ? "mail: %m!" : "mail group not found!");
return EX_OSERR;
}
gbx = gi->gr_gid;
strcat(inbox, rcp);
if (verbose)
INF("Delivering mail from %s to inbox %s.", sdr, inbox);
return msg_write(inbox, ubx, gbx, sdr);
}
/** Main **/
int main(int argc, char *argv[])
{
char c;
int ret;
setlocale(LC_ALL, "");
if (argv == NULL || argv[0] == NULL)
return EX_USAGE;
prg = basename(argv[0]);
if (!strcmp(prg, "mailq") || !strcmp(prg, "newaliases"))
return 0;
/* Read command line: sendmail [-opt] [rcp] */
opterr = 0;
while ((c = getopt(argc, argv, "tivo:b:f:")) != -1)
switch (c) {
case 't': scan_rcp = 1; break;
case 'v': verbose = 1; break;
case 'o': if (*optarg != 'i') break; /* Fallthru */
case 'i': ign_dot = 1; break;
case 'f': sdr = optarg; break;
case 'b': if (*optarg != 'm') return 0; break;
case '?': help(); return EX_USAGE;
}
if (optind < argc) {
rcp = argv[optind];
scan_rcp = 0;
}
/* Write stdin to inbox */
abinit();
ret = msg_copy();
abdone();
return ret;
}