/*
 * 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;
  }