From 2523835aaccc6663f9884b82c778abcccaddbd82 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Mon, 8 Jul 2013 22:18:56 +0200 Subject: Initial commit --- .gitignore | 1 + Makefile | 35 +++++++++ README.md | 76 ++++++++++++++++++++ femtomail.c | 233 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 femtomail.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d66aac --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +femtomail diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8fe0fc5 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +SBINDIR ?= /usr/sbin +OBJDIR ?= . +DESTDIR ?= + +INSTALL ?= install +RM ?= rm +LN ?= ln +SETCAP ?= setcap + +CFLAGS ?= -Wall -Wextra -O2 -g +ifneq ($(MAILBOX_PATH),) + override CFLAGS += -DMAILBOX_PATH="$(MAILBOX_PATH)" +endif + +all: $(OBJDIR)/femtomail +.PHONY: all install install-link-sendmail setcap clean + +$(OBJDIR)/femtomail: femtomail.c +ifeq ($(USERNAME),) + $(error USERNAME must be set and non-empty) +endif + $(CC) -DUSERNAME="$(USERNAME)" $(CFLAGS) -o $(DESTDIR)$@ $< + +clean: + $(RM) $(OBJDIR)/femtomail + +install: $(OBJDIR)/femtomail + $(INSTALL) -m 755 -d $(DESTDIR)$(SBINDIR) + $(INSTALL) -m 755 $(OBJDIR)/femtomail $(DESTDIR)$(SBINDIR)/femtomail + +install-link-sendmail: install + $(LN) -s femtomail $(DESTDIR)$(SBINDIR)/sendmail + +setcap: install + $(SETCAP) cap_setuid,cap_setgid=ep $(DESTDIR)$(SBINDIR)/femtomail diff --git a/README.md b/README.md new file mode 100644 index 0000000..379dca7 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +femtomail - minimal MDA with Maildir support +============================================ + +femtomail is a minimal Mail Delivery Agent (MDA) for local mail. Mail is +accepted from standard input and placed in a Maildir box of a user. This +software is intended for use on a single-user machine. + +Remote delivery, daemonizing, sender verification, etc. is not implemented and +won't be implemented due to its complexity. femtomail is not written because +mail software did not exist, but because existing software were too large for +the simple task of [delivering cron mail to the local user][1]. + +The workflow of femtomail: + + 1. Change the process user and group. + 2. Create a new file with a [unique filename][2] in the mail directory. + 3. Write a `Received` header to the file. + 4. Pass data from standard input to the file. + 5. Exit. + + +Installation +------------ +The user to deliver mail to has to be specified at compile time: + + make USERNAME=peter + +By default, the Maildir directory is `~/.local/share/local-mail/inbox`. It can +be changed to `~/.Maildir/inbox` as follows: + + make USERNAME=peter MAILBOX_PATH=.Maildir/inbox + +To install femtomail on your system with the appropriate capabilities: + + make install install-link-sendmail setcap + +Note: the femtomail binary must be installed with file capabilities set +(recommended). Alternatively, the program can run with setuid-root. Either way, +the user and groups are changed before the mail is read and written. + + +Usage +----- +If you do not have appropriate privileges to install femtomail (you are not +root) or if you want to try it out before installing, then you can specify +the program as sendmail program for the `mail` program (from +[`heirloom-mailx`][2]). + +Example (assuming that `femtomail` is built and available in the current working +directory): + + echo Testing... | mail -S sendmail=femtomail -s Subject peter + + +Bugs +---- +This program does not parse any sendmail option. All arguments are ignored, +except the (optional) first address (which is written in the `Received` mail +header). No validation is performed at this address, the mail headers and its +body. If femtomail is invoked without specifying mail contents, an empty message +will be created. + +Other bugs can be reported at . + + +Copyright +--------- +Copyright (c) 2013 Peter Wu + +License: GNU GPL version 3 or later . This is +free software: you are free to change and redistribute it. There is NO WARRANTY, +to the extent permitted by law. + + + [1]: http://unix.stackexchange.com/q/82093/8250 + [2]: http://heirloom.sourceforge.net/mailx.html diff --git a/femtomail.c b/femtomail.c new file mode 100644 index 0000000..6bc1fb8 --- /dev/null +++ b/femtomail.c @@ -0,0 +1,233 @@ +/** + * femtomail - Minimal sendmail replacement for forwarding mail to a single + * Maildir box. Note: this program does not try to implement sendmail. + * + * Installation commands: + * $ cc -DUSERNAME=$USER femtomail.c -o femtomail + * (Optional override: -DMAILBOX_PATH=.Maildir/inbox) + * # install -m 755 femtomail /usr/bin/sendmail + * # setcap cap_setuid,cap_setgid=ep /usr/bin/sendmail + * + * Copyright (C) 2013 Peter Wu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * 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, see . + */ + +#define _GNU_SOURCE /* for setresgid/setresuid */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STRINGIFY(str) #str + +#ifndef USERNAME +# error Please define the user to deliver mail to with USERNAME +#endif + +/* Maildir directory relative to home dir of USERNAME (see above) */ +#ifndef MAILBOX_PATH +# define MAILBOX_PATH .local/share/local-mail/inbox +#endif + +/* change user/group context to username and fill in maildir path */ +int +init_user(const char *username, char *maildir, size_t maildir_len) { + struct passwd *pwd; + uid_t uid; + gid_t gid; + + if ((pwd = getpwnam(username)) == NULL) { + fprintf(stderr, "Unknown user %s, cannot locate Maildir.\n", username); + return 1; + } + + uid = pwd->pw_uid; + gid = pwd->pw_gid; + + if (setresgid(gid, gid, gid) || setresuid(uid, uid, uid)) { + perror("Failed to change uid/gid"); + return 1; + } + + snprintf(maildir, maildir_len, "%s/" STRINGIFY(MAILBOX_PATH) "/new", pwd->pw_dir); + return 0; +} + +/* get a random number between 0 and 999 (inclusive) */ +unsigned +get_random(void) { + FILE *fp; + int rnd; + + fp = fopen("/dev/urandom", "r"); + if (fp != NULL) { + unsigned char buf[2]; + + fread(buf, 2, 1, fp); + rnd = (buf[0] << 8) | buf[1]; + + fclose(fp); + } else { + rnd = rand(); + } + + return rnd % 1000; +} + +/* get the current hostname, sanitizing '/' and ':' chars */ +void +get_hostname(char *hostname, size_t hostname_len) { + if (gethostname(hostname, hostname_len) >= 0) { + char *p; + /* sanitized per http://cr.yp.to/proto/maildir.html */ + for (p = hostname; *p; p++) { + if (*p == '/') *p = '\057'; + if (*p == ':') *p = '\072'; + } + } else { + strncpy(hostname, "unknown", hostname_len); + } +} + +/* generate a filename suitable for Maildir consisting of a timestamp, a random + * middle part and a hostname (per http://cr.yp.to/proto/maildir.html) */ +void +make_name(char *name, size_t name_len, time_t tm) { + char hostname[128]; + + get_hostname(hostname, sizeof(hostname)); + snprintf(name, name_len, "%llu.R%u.%s", + (unsigned long long) tm, get_random(), hostname); +} + +/* write Received header to file */ +void +write_headers(FILE *mail_fp, const char *address, time_t tm) { + char timestr[200]; + struct tm *tmp; + + tmp = localtime(&tm); + if (tmp == NULL) { + perror("localtime"); + return; + } + + if (!strftime(timestr, sizeof(timestr), "%a, %d %b %Y %T %z", tmp)) { + fprintf(stderr, "strftime() failed!\n"); + return; + } + + fprintf(mail_fp, "Received: for %s with local (femtomail); %s\n", address, timestr); +} + +/* read text from stdin and write to file */ +int +read_and_write(FILE *mail_fp) { + size_t r; + char buf[4096]; + + while ((r = fread(buf, 1, sizeof(buf), stdin)) > 0) { + if (fwrite(buf, 1, r, mail_fp) != r) { + /* disk full? */ + return 1; + } + } + + return 0; +} + +/* validate the recipient address as in `sendmail [address]` */ +bool +valid_address(const char *address) { + char c; + + while ((c = *address++) != 0) { + /* reject unprintable chars, including newlines */ + if (!isgraph(c)) { + return false; + } + } + + return true; +} + +int +main(int argc, char **argv) { + char maildir[256]; + char *address = NULL, file_path[256]; + time_t tm; + int i, mail_fd, ret; + FILE *mail_fp; + + for (i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + /* ignore arg, hopefully next arg does not contain a value */ + continue; + } + + if (!valid_address(argv[i])) { + fprintf(stderr, "Illegal characters in address!\n"); + return (EXIT_FAILURE); + } + + address = argv[i]; + break; + } + + if (!address) { + fprintf(stderr, "Missing recipient address.\n"); + return (EXIT_FAILURE); + } + + if (init_user(STRINGIFY(USERNAME), maildir, sizeof(maildir))) { + return (EXIT_FAILURE); + } + + if (chdir(maildir)) { + fprintf(stderr, "chdir(%s): %s\n", maildir, strerror(errno)); + return (EXIT_FAILURE); + } + + tm = time(NULL); + make_name(file_path, sizeof(file_path), tm); + + /* ensure that no file gets overwritten */ + mail_fd = open(file_path, O_WRONLY | O_CREAT | O_EXCL, 0644); + if (mail_fd < 0) { + perror("open"); + return (EXIT_FAILURE); + } + + mail_fp = fdopen(mail_fd, "w"); + + write_headers(mail_fp, address, tm); + ret = read_and_write(mail_fp); + if (ret) { + perror("fwrite"); + } + + fclose(mail_fp); + return ret; +} + +/* vim: set sw=4 ts=4 et: */ -- cgit v1.2.1