summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Wu <lekensteyn@gmail.com>2013-07-08 22:18:56 +0200
committerPeter Wu <lekensteyn@gmail.com>2013-07-09 00:08:23 +0200
commit2523835aaccc6663f9884b82c778abcccaddbd82 (patch)
treeba00c3221aa8bbccb83c79e6f418c8633031696a
downloadfemtomail-2523835aaccc6663f9884b82c778abcccaddbd82.tar.gz
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--Makefile35
-rw-r--r--README.md76
-rw-r--r--femtomail.c233
4 files changed, 345 insertions, 0 deletions
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 <lekensteyn@gmail.com>.
+
+
+Copyright
+---------
+Copyright (c) 2013 Peter Wu <lekensteyn@gmail.com>
+
+License: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. 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 <lekensteyn@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#define _GNU_SOURCE /* for setresgid/setresuid */
+#include <time.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <sys/types.h>
+#include <pwd.h>
+#include <sys/prctl.h>
+
+#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: */