cdb-check-domain cdb-probe not qmail-checkspam
+gorp if-mtu locking
##
## No proper build system here. Just kludgy hacks.
+VERSION = 1.1.0
+
+SCRIPTS = \
+ cdb-assign cdb-list \
+ unfwd splitconf
+
PROGS = \
qmail-checkspam not \
cdb-probe cdb-check-domain \
+ gorp \
+ locking if-mtu
+
+OTHERS = \
xtitle.so
+MAN1 = \
+ not.1 \
+ cdb-assign.1 cdb-list.1 cdb-probe.1 cdb-check-domain.1 \
+ gorp.1 \
+ unfwd.1 splitconf.1 locking.1 if-mtu.1
+
+MAN8 = \
+ qmail-checkspam.8
+
CC = gcc
LD = gcc
-CFLAGS = -O2 -g -pedantic -Wall
+CFLAGS = -O2 -g -pedantic -Wall -DVERSION=\"$(VERSION)\"
LINK = $(LD) $(LDFLAGS) -o $@ $^
-all: $(PROGS)
+prefix = /usr/local
+bindir = $(prefix)/bin
+mandir = $(prefix)/man
+man1dir = $(mandir)/man1
+man8dir = $(mandir)/man8
+libdir = $(prefix)/lib
+
+all: $(PROGS) $(OTHERS)
+
+install: all
+ [ -d $(bindir) ] || install -d $(bindir)
+ install -m775 $(PROGS) $(SCRIPTS) $(bindir)
+ [ -d $(man1dir) ] || install -d $(man1dir)
+ install -m644 $(MAN1) $(man1dir)
+ [ -d $(man8dir) ] || install -d $(man8dir)
+ install -m644 $(MAN8) $(man8dir)
+ [ -d $(libdir) ] || install -d $(libdir)
+ install -m644 xtitle.so $(libdir)
qmail-checkspam: qmail-checkspam.o
$(LINK) -lspamc
xtitle.so: xtitle.o
$(LINK) -shared
-clean:; rm -f *.o $(PROGS)
+gorp: gorp.o
+ $(LINK) -lcatacomb -lmLib
+
+locking: locking.o
+ $(LINK) -lmLib
+
+if-mtu: if-mtu.o
+ $(LINK)
+
+clean:; rm -f *.o *~ $(PROGS) $(OTHERS)
-.PHONY: all clean
+.PHONY: all clean install
--- /dev/null
+.\" -*-nroff-*-
+.TH cdb-assign 1 "9 October 2003" "Straylight/Edgeware"
+.SH SYNOPSIS
+.B cdb-assign
+.I cdb
+.RI [ input ...]
+.SH DESCRIPTION
+Constructs a CDB file (see
+.BR cdbmake (1),
+.BR cdbget (1))
+from the assignments in the
+.I input
+files (or stdin if there are none).
+.PP
+Comments and blank lines in the inputs are ignored; other lines must
+have the form
+.RI ` key
+.B =
+.IR value '. Leading and trailing spaces in each are stripped. This is
+a convenient tool for building simple CBD files, rather than
+comprehensive one.
+.PP
+The CDB file is written to
+.IB cdb .new
+and renamed to
+.I cdb
+when it's finished.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding, <mdw@nsict.org>
--- /dev/null
+.\" -*-nroff-*-
+.TH cdb-check-domain 1 "10 October 2003" "Straylight/Edgeware"
+.SH NAME
+cdb-check-domain \- check if a domain name exists in a CDB file
+.SH SYNOPSIS
+.B cdb-check-domain
+.I dom
+.BI < cdb
+.SH DESCRIPTION
+If the key
+.IR dom ,
+or any key
+.BI . suff
+which is a right substring of
+.I dom
+exists in the CDB file
+.I cdb
+then exit 0; if it doesn't exist, exit 1. If anything bad happened,
+then exit 111.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding, <mdw@nisict.org>
--- /dev/null
+.\" -*-nroff-*-
+.TH cdb-list 1 "9 October 2003" "Straylight/Edgeware"
+.SH SYNOPSIS
+.B cdb-list
+.I cdb
+.RI [ input ...]
+.SH DESCRIPTION
+Constructs a CDB file (see
+.BR cdbmake (1),
+.BR cdbget (1))
+from a list of items in the
+.I input
+files (or stdin if there are none).
+.PP
+Comments and blank lines in the inputs are ignored. Each other line
+.I line
+causes an entry to be inserted into the BDB, whose key is
+.I line
+with leading and trailing spaces stripped, and whose value is empty.
+.PP
+The CDB file is written to
+.IB cdb .new
+and renamed to
+.I cdb
+when it's finished.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding, <mdw@nsict.org>
--- /dev/null
+.\" -*-nroff-*-
+.TH cdb-probe 1 "10 October 2003" "Straylight/Edgeware"
+.SH NAME
+cdb-probe \- check if a key exists in a CDB file
+.SH SYNOPSIS
+.B cdb-probe
+.I key
+.BI < cdb
+.SH DESCRIPTION
+If the key
+.I key
+exists in the CDB file
+.I cdb
+then exit 0; if it doesn't exist, exit 1. If anything bad happened,
+then exit 111.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding, <mdw@nisict.org>
--- /dev/null
+#! /bin/sh
+bouncesaying "fatal: sending to this address forbidden" \
+ not cdb-check-domain "${SENDER##*@}"
--- /dev/null
+.\" -*-nroff-*-
+.TH check-sender 1 "9 October 2003" "Straylight/Edgeware"
+.SH NAME
+check-sender \- check an envelope sender's domain in a CDB file
+.SH SYNOPSIS
+(In a
+.B .qmail
+file)
+.br
+.B |check-sender
+.BI < cdb
+.SH DESCRIPTION
+If the domain part of the email address in the
+.B SENDER
+environment variable is matched in
+.I cdb
+according to
+.BR cdb-check-domain (1)
+then exit 0. Otherwise, bounce the message with a refusal message.
+.SH BUGS
+None known
+.SH AUTHOR
+Mark Wooding, <mdw@nisict.org>
--- /dev/null
+.\" -*-nroff-*-
+.TH gorp 1 "9 October 2003" "Straylight/Edgeware"
+.SH NAME
+gorp \- write a short random string
+.SH SYNOPSIS
+.B gorp
+.RI [ bits ]
+.SH DESCRIPTION
+Generates
+.I bits
+random bits (must be a multiple of 8, default is 128) and writes the
+resulting string to standard output in base64 format.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding <mdw@nsict.org>
--- /dev/null
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <mLib/alloc.h>
+#include <mLib/base64.h>
+#include <mLib/dstr.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+
+#include <catacomb/rand.h>
+#include <catacomb/noise.h>
+
+int main(int argc, char *argv[])
+{
+ dstr d = DSTR_INIT;
+ base64_ctx b;
+ char *p;
+ size_t n;
+
+ ego(argv[0]);
+ if (argc > 2) {
+ pquis(stderr, "Usage: $ [BITS]\n");
+ exit(EXIT_FAILURE);
+ }
+ n = argc == 2 ? atoi(argv[1]) : 128;
+ if (!n || n % 8) die(EXIT_FAILURE, "bad bit count");
+ n >>= 3;
+ p = xmalloc(n);
+ rand_noisesrc(RAND_GLOBAL, &noise_source);
+ rand_seed(RAND_GLOBAL, 160);
+ rand_get(RAND_GLOBAL, p, n);
+ base64_init(&b);
+ b.maxline = 0;
+ b.indent = "";
+ base64_encode(&b, p, n, &d);
+ base64_encode(&b, 0, 0, &d);
+ printf("%s\n", d.buf);
+ return (0);
+}
--- /dev/null
+.TH "if-mtu" 1 "24 September 2003" "Mark Wooding" "Toys"
+.SH NAME
+if-mtu \- find MTU of an interface
+.
+.SH SYNOPSIS
+.B if\-mtu
+.I iface
+.
+.SH "DESCRIPTION"
+The
+.B if\-mtu
+prints the MTU of the network interface
+.I iface
+to standard output, and exits.
+.
+.SH "BUGS"
+None.
+.
+.SH "AUTHOR"
+Mark Wooding, <mdw@nsict.org>
--- /dev/null
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+
+int main(int argc, char *argv[])
+{
+ int fd;
+ struct ifreq ifr;
+ if (argc != 2) {
+ fprintf(stderr, "usage: %s IFACE\n", argv[0]);
+ exit(1);
+ }
+ if (strlen(argv[1]) >= sizeof(ifr.ifr_name)) {
+ fprintf(stderr, "%s: interface name `%s' too long\n", argv[0], argv[1]);
+ exit(1);
+ }
+ if ((fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
+ fprintf(stderr, "%s: socket: %s\n", argv[0], strerror(errno));
+ exit(1);
+ }
+ strcpy(ifr.ifr_name, argv[1]);
+ if (ioctl(fd, SIOCGIFMTU, &ifr)) {
+ fprintf(stderr, "%s: ioctl(SIOCGIFMTU): %s\n", argv[0], strerror(errno));
+ exit(1);
+ }
+ printf("%d\n", ifr.ifr_mtu);
+ return (0);
+}
--- /dev/null
+.TH "locking" 1 "11 May 2003" "Mark Wooding" "Toys"
+.SH NAME
+locking \- run a program with a lock held
+.
+.SH SYNOPSIS
+.B locking
+.RB [ \-cfwx ]
+.RB [ \-p
+.IR realprog ]
+.RB [ \-t
+.IR time ]
+.I file
+.I prog
+.IR args ...
+.
+.SH "DESCRIPTION"
+The
+.B locking
+program opens and locks
+.I file
+while it runs some program
+.IR prog .
+This is handy in shell scripts and (particularly) crontab entries. It
+was initially written to ensure that news fetches only occurred if the
+previous one had completed.
+.PP
+The program uses
+.BR fcntl (2)-style
+locking. It will not leave stale locks lying around: the lock is
+released if
+.I prog
+or the
+.B locking
+program itself quits for any reason.
+.PP
+The following options are supported:
+.TP
+.B "\-h, \-\-help"
+Write a full help message to stdout, and exit zero.
+.TP
+.B "\-v, \-\-version"
+Write the version number to stdout, and exit zero.
+.TP
+.B "\-u, \-\-usage"
+Write a brief usage message to stdout, and exit zero.
+.TP
+.BR "\-c, \-\-" [ "no\-" ] "create"
+Create
+.I file
+if it doesn't already exist. The file is created with mode 666 as
+modified by the process's umask in the usual way. This is the default
+behaviour. The options
+.B +c
+or
+.B \-\-no\-create
+will cause
+.B locking
+to fail if the file does not already exist.
+.TP
+.BR "\-f, \-\-" [ "no\-" ] "fail"
+Fail (report an error and exit nonzero) if
+.I file
+is already locked. The default behaviour is to exit successfully
+without running
+.IR prog .
+.TP
+.BR "\-w, \-\-" [ "no\-" ] "wait"
+Wait for the lock on
+.I file
+to become available. The default behaviour is to give up immediately.
+.TP
+.BR "\-x, \-\-" [ "no\-" ] "exclusive"
+Obtain an exclusive lock on the file. This is the default. The options
+.B +x
+or
+.B \-\-no\-exclusive
+obtain a non-exclusive lock on the file instead.
+.TP
+.BI "\-p, \-\-program=" realprog
+Run the program
+.IR realprog ,
+passing it
+.I prog
+as its first argument. The default behaviour is to run
+.IR prog .
+.TP
+.BI "\-t, \-\-timeout=" time
+Wait for
+.I time
+(an integer, optionally followed by
+.RB ` d ',
+.RB ` h ',
+.RB ` m ',
+or
+.RB ` s '
+for days, hours, minutes or seconds, respectively) for the lock to
+become available, and then give up. This only makes sense with the
+.B \-\-wait
+option, so that is turned on automatically.
+.SH "BUGS"
+The
+.B locking
+program messes with alarms. It tries to put them back the way it found
+them, but may get things wrong.
+.
+.SH "AUTHOR"
+Mark Wooding, <mdw@nsict.org>
--- /dev/null
+/* -*-c-*-
+ *
+ * $Id: locking.c,v 1.1 2003/10/09 15:05:34 mdw Exp $
+ *
+ * Lock a file, run a program
+ *
+ * (c) 2003 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Toys utilties collection.
+ *
+ * Toys 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Toys 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 Toys; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Revision history --------------------------------------------------*
+ *
+ * $Log: locking.c,v $
+ * Revision 1.1 2003/10/09 15:05:34 mdw
+ * Lots of stuff.
+ *
+ * Revision 1.3 2003/09/24 14:58:08 mdw
+ * Fix options parsing again.
+ *
+ * Revision 1.2 2003/09/24 14:14:03 mdw
+ * Fix option handling behaviour.
+ *
+ * Revision 1.1 2003/05/11 13:30:04 mdw
+ * Initial checkin.
+ *
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <errno.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/wait.h>
+
+#include <mLib/mdwopt.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+
+/*----- Static variables --------------------------------------------------*/
+
+static jmp_buf jmp;
+
+/*----- Main code ---------------------------------------------------------*/
+
+static void alrm(int s) { longjmp(jmp, 1); }
+
+static void usage(FILE *fp)
+{
+ pquis(fp,
+ "Usage: $ [-cfwx] [-p REALPROG] [-t TIMEOUT] FILE PROG ARGS...\n");
+}
+
+static void version(FILE *fp)
+{
+ pquis(fp, "$ (toys, version " VERSION "\n");
+}
+
+static void help(FILE *fp)
+{
+ version(fp);
+ putchar('\n');
+ usage(fp);
+ pquis(fp, "\n\
+Lock FILE and run PROG, passing ARGS. Options are:\n\
+\n\
+-h, --help Show this help message.\n\
+-v, --version Show version string.\n\
+-u, --usage Show terse usage summary.\n\
+\n\
+-c, --[no-]create Create FILE if it doesn't exist [default: on].\n\
+-f, --[no-]fail Fail if the file is already locked [default: off].\n\
+-w, --[no-]wait Wait for the lock to be available [default: off].\n\
+-x, --[no-]exclusive Get an exclusive (writer) lock [default: on].\n\
+-p, --program=REALPROG Run REALPROG instead of PROG.\n\
+-t, --timeout=TIME Wait for TIME for lock to become available.\n\
+");
+}
+
+int main(int argc, char *argv[])
+{
+ const char *file = 0;
+ const char *prog = 0;
+ char *const *av;
+ void (*oalrm)(int) = 0;
+ int fd;
+ struct flock l;
+ char *p;
+ int t = -1;
+ unsigned int ot;
+ time_t nt;
+ pid_t kid;
+ int st;
+
+#define f_bogus 1u
+#define f_wait 2u
+#define f_fail 4u
+#define f_create 8u
+#define f_excl 16u
+
+ unsigned f = f_create | f_excl;
+
+ ego(argv[0]);
+
+ for (;;) {
+ static const struct option opts[] = {
+ { "help", 0, 0, 'h' },
+ { "version", 0, 0, 'v' },
+ { "usage", 0, 0, 'u' },
+ { "wait", OPTF_NEGATE, 0, 'w' },
+ { "fail", OPTF_NEGATE, 0, 'f' },
+ { "create", OPTF_NEGATE, 0, 'c' },
+ { "program", OPTF_ARGREQ, 0, 'p' },
+ { "timeout", OPTF_ARGREQ, 0, 't' },
+ { "exclusive", OPTF_NEGATE, 0, 'x' },
+ { 0, 0, 0, 0 }
+ };
+
+ int i = mdwopt(argc, argv, "-hvuw+f+c+x+p:t:", opts,
+ 0, 0, OPTF_NEGATION);
+ if (i < 0)
+ break;
+ switch (i) {
+ case 'h':
+ help(stdout);
+ exit(0);
+ case 'v':
+ version(stdout);
+ exit(0);
+ case 'u':
+ usage(stdout);
+ exit(0);
+ case 'w':
+ f |= f_wait;
+ break;
+ case 'w' | OPTF_NEGATED:
+ f &= ~f_wait;
+ break;
+ case 'f':
+ f |= f_fail;
+ break;
+ case 'f' | OPTF_NEGATED:
+ f &= ~f_fail;
+ break;
+ case 'c':
+ f |= f_create;
+ break;
+ case 'c' | OPTF_NEGATED:
+ f &= ~f_create;
+ break;
+ case 'x':
+ f |= f_excl;
+ break;
+ case 'x' | OPTF_NEGATED:
+ f &= ~f_excl;
+ break;
+ case 't':
+ errno = 0;
+ t = strtol(optarg, &p, 0);
+ switch (*p) {
+ case 'd': t *= 24;
+ case 'h': t *= 60;
+ case 'm': t *= 60;
+ case 's': p++;
+ case 0: break;
+ default: die(111, "unknown time unit `%c'", *p);
+ }
+ if (*p || t < 0 || errno)
+ die(111, "bad time value `%s'", optarg);
+ f |= f_wait;
+ break;
+ case 'p':
+ prog = optarg;
+ break;
+ case 0:
+ if (file) {
+ optind--;
+ goto doneopts;
+ }
+ file = optarg;
+ break;
+ default:
+ f |= f_bogus;
+ break;
+ }
+ }
+
+doneopts:
+ if (f & f_bogus || argc - optind < 1) {
+ usage(stderr);
+ exit(EXIT_FAILURE);
+ }
+
+ av = &argv[optind];
+ if (!prog)
+ prog = av[0];
+ if ((fd = open(file,
+ ((f & f_create ? O_CREAT : 0) |
+ (f & f_excl ? O_RDWR : O_RDONLY)), 0666)) < 0)
+ die(111, "error opening `%s': %s", file, strerror(errno));
+ l.l_type = f & f_excl ? F_WRLCK : F_RDLCK;
+ l.l_whence = SEEK_SET;
+ l.l_start = 0;
+ l.l_len = 0;
+ nt = time(0);
+ if (setjmp(jmp)) {
+ errno = EAGAIN;
+ nt = t;
+ } else {
+ ot = alarm(0);
+ oalrm = signal(SIGALRM, alrm);
+ if (t >= 0)
+ alarm(t);
+ if (fcntl(fd, f & f_wait ? F_SETLKW : F_SETLK, &l) >= 0)
+ errno = 0;
+ }
+ signal(SIGALRM, oalrm);
+ if (ot) {
+ nt = time(0) - nt;
+ if (nt > ot)
+ raise(SIGALRM);
+ else
+ alarm(ot - nt);
+ }
+ if (errno &&
+ ((errno != EAGAIN && errno != EWOULDBLOCK && errno != EACCES) ||
+ (f & f_fail)))
+ die(111, "error locking `%s': %s", file, strerror(errno));
+ if (errno)
+ exit(0);
+
+ if ((kid = fork()) < 0)
+ die(111, "error from fork: %s", strerror(errno));
+ if (!kid) {
+ close(fd);
+ execvp(prog, av);
+ die(111, "couldn't exec `%s': %s", prog, strerror(errno));
+ }
+ if (waitpid(kid, &st, 0) < 0)
+ die(EXIT_FAILURE, "error from wait: %s", strerror(errno));
+ l.l_type = F_UNLCK;
+ l.l_whence = SEEK_SET;
+ l.l_start = 0;
+ l.l_len = 0;
+ fcntl(fd, F_SETLK, &l);
+ close(fd);
+ if (WIFEXITED(st))
+ exit(WEXITSTATUS(st));
+ else
+ exit(255);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
--- /dev/null
+.\" -*-nroff-*-
+.TH not 1 "9 October 2003" "Straylight/Edgeware"
+.SH NAME
+not \- invert the sense of a program
+.SH SYNOPSIS
+.B not
+.I program
+.IR args ...
+.SH DESCRIPTION
+Runs
+.IR program ,
+passing it
+.IR args .
+If
+.I program
+exits 111, or there's some kind of problem, then
+.B not
+exits 111; if
+.I program
+exits 0 then
+.B not
+exits 100; otherwise
+.B not
+exits 0. This is useful in
+.BR bouncesaying (1)
+lines.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding, <mdw@nsict.org>
--- /dev/null
+.\" -*-nroff-*-
+.TH qmail-checkspam 8 "9 October 2003" "Straylight/Edgeware"
+.SH NAME
+qmail-checkspam \- check messages for spam before queueing them
+.SH SYNOPSIS
+.B qmail-checkspam
+.SH DESCRIPTION
+The
+.B qmail-checkspam
+program provides the same interface as
+.BR qmail-queue (8).
+It reads a message on file descriptor 0, and passes it to
+.BR spamd (8).
+If
+.B spamd
+thinks the message is spam, then
+.B qmail-checkspam
+exits 31, indicating a permanent refusal.
+Otherwise,
+.B qmail-checkspam
+feeds the message, augmented by SpamAssassin's
+.B X-Spam-Status
+header, to
+.BR qmail-queue (8).
+.PP
+You will usually run
+.B qmail-checkspam
+by applying the
+.B QMAILQUEUE
+patch to
+.B qmail (7)
+and setting the
+.B QMAILQUEUE
+environment variable to refer to
+.B qmail-checkspam
+in, e.g., the
+.B /etc/hosts.deny
+file (see
+.BR hosts_options (5)).
+.SH ENVIRONMENT
+.TP
+.B QMAIL_CHECKSPAM_SPAMDHOST
+The hostname or IP address of the
+.B spamd
+to connect to. Default is
+.BR localhost .
+.TP
+.B QMAIL_CHECKSPAM_SPAMDPORT
+The port number to connect to. The default is
+.BR 783 .
+.TP
+.B QMAIL_CHECKSPAM_THRESH
+The threshold score for rejection. The default is whatever
+.B spamd
+is configured to.
+.TP
+.B QMAIL_CHECKSPAM_QUEUE
+The
+.BR qmail-queue -compatible
+program to pass good mail to. The default is
+.BR /var/qmail/bin/qmail-queue .
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding, <mdw@nsict.org>
+
#! /usr/bin/tclsh
+#----- Miscellaneous utilities ----------------------------------------------
+
+# die MSG
+#
+# Something didn't work. Exit right now.
+
proc die {msg} {
global argv0
puts stderr "$argv0: $msg"
exit 1
}
+# usage FILE
+#
+# Write a usage message to FILE, which is a file handle.
+
proc usage {file} {
global argv0
puts $file "Usage: \n\
$argv0 -u OUTPUT FILE FILE ..."
}
-set job "split"
-while {[llength $argv]} {
- switch -glob -- [lindex $argv 0] {
- "-u" - "--unsplit" {
- set job "unsplit"
- if {[llength $argv] < 2} { die "option `-u' needs an argument" }
- set output [lindex $argv 1]
- set argv [lrange $argv 1 end]
- }
- "-d" - "--delete" { set job "delete" }
- "-s" - "--split" { set job "split" }
- "-h" - "--help" { usage stdout; exit 0 }
- "-" { break }
- "--" { set argv [lrange $argv 1 end]; break }
- "-*" { die "unknown option `[lindex $argv 0]'"; exit 1 }
- default { break }
- }
- set argv [lrange $argv 1 end]
-}
+# clear-arrays ARRAY ...
+#
+# Make each named ARRAY exist and be empty.
proc clear-arrays {args} {
foreach i $args {
}
}
+#------ Write-safe ----------------------------------------------------------
+
+# write-safe STUFF [TIDY]
+#
+# Do some safe I/O. If STUFF succeeds, do TIDY and commit the modifications;
+# otherwise, do TIDY and back out all the changes. See also write-safe-open,
+# write-safe-file and write-safe-delete.
+
proc write-safe {stuff {tidy {}}} {
global _ws_close _ws_del _ws_new
clear-arrays _ws_del _ws_new
return {}
}
+# write-safe-open NAME [TRANS]
+#
+# Open file NAME for writing, with the translation mode TRANS (default is
+# `auto'); return the file handle. The file NAME is not destroyed until the
+# changes are committed by an enclosing write-safe completing. You can close
+# the file handle if you like; write-safe will close it automatically anyway.
+
proc write-safe-open {name {trans auto}} {
global _ws_close _ws_new
if {[file isdirectory $name]} { error "`$name' is a directory" }
return $f
}
+# write-safe-delete NAME
+#
+# Delete file NAME. The file isn't actually removed until the enclosing
+# write-safe completes.
+
proc write-safe-delete {name} {
global _ws_del
set _ws_del($name) 0
}
+# write-safe-file NAME CONTENTS [TRANS]
+#
+# Write CONTENTS to FILE, using translation mode TRANS (default `auto'). The
+# file isn't actually replaced until the changes are committed by an
+# enclosing write-safe completing.
+
proc write-safe-file {name contents {trans auto}} {
set f [write-safe-open $name $trans]
puts -nonewline $f $contents
close $f
}
+# read-file NAME [TRANS]
+#
+# Evaluates to the contents of the file NAME under translation mode TRANS
+# (default `auto').
+
proc read-file {name {trans auto}} {
set f [open $name]
fconfigure $f -translation $trans
return $c
}
+#----- Splitconf-specific stuff ---------------------------------------------
+
+# write-safe-manifest F L
+#
+# Writes the list of filenames L to the manifest file associated with config
+# file F.
+
proc write-safe-manifest {f l} {
set f [write-safe-open $f.files]
foreach i $l { puts $f $i }
close $f
}
+# old-files CONF
+#
+# Returns the filenames in the current manifest of the config file CONF.
+
proc old-files {conf} {
set old {}
if {[file exists $conf.files]} {
return $old
}
+#----- Main code ------------------------------------------------------------
+
+set job "split"
+while {[llength $argv]} {
+ switch -glob -- [lindex $argv 0] {
+ "-u" - "--unsplit" {
+ set job "unsplit"
+ if {[llength $argv] < 2} { die "option `-u' needs an argument" }
+ set output [lindex $argv 1]
+ set argv [lrange $argv 1 end]
+ }
+ "-d" - "--delete" { set job "delete" }
+ "-s" - "--split" { set job "split" }
+ "-h" - "--help" { usage stdout; exit 0 }
+ "-" { break }
+ "--" { set argv [lrange $argv 1 end]; break }
+ "-*" { die "unknown option `[lindex $argv 0]'"; exit 1 }
+ default { break }
+ }
+ set argv [lrange $argv 1 end]
+}
+
set rc 0
clear-arrays opt
array set opt {
set old [old-files $conf]
set c [open $conf r]
catch { unset o }
- set file ""
set spill ""
+ set donebefore 0
array set new {}
write-safe {
while {[gets $c line] >= 0} {
if {[regexp -- {^\[(.*)\]\s*$} $line . name]} {
if {[info exists o]} {
- puts -nonewline $o $file
close $o
+ } elseif {!$donebefore} {
+ exec "sh" "-c" $opt(before) <@ stdin >@ stdout 2>@ stderr
+ set donebefore 1
+ }
+ if {[string equal $name ""]} {
+ catch { unset o }
} else {
- exec "sh" "-c" $opt(before)
+ set name "$opt(prefix)$name"
+ set o [write-safe-open $name]
+ set new($name) 1
+ set spill ""
}
- set name "$opt(prefix)$name"
- set o [write-safe-open $name]
- set new($name) 1
- set file ""
- set spill ""
} elseif {[info exists o]} {
switch -regexp -- $line {
{^\s*$} { append spill "$line\n" }
{^\#\#} { }
- {^\!} { append file "$spill[string range $line 1 end]\n" }
- default { append file "$spill$line\n" }
+ {^\!} {
+ puts -nonewline $o "$spill[string range $line 1 end]\n"
+ set spill ""
+ }
+ default { puts -nonewline $o "$spill$line\n"; set spill "" }
}
} elseif {[regexp -- {^\s*(\#|$)} $line]} {
continue
- } elseif {[regexp -- {^\s*([-\w]+)\s*=\s*(.*\S|)\s*$} $line . k v]} {
+ } elseif {[regexp -- \
+ {^\s*([-./\w]+)\s*=\s*(.*\S|)\s*$} $line . k v]} {
if {![info exists opt($k)]} {
error "unknown configuration option `$k'"
} else {
set opt($k) $v
}
+ } elseif {[regexp -- \
+ {^\s*([-./\w]+)\s*:\s*(.*\S|)\s*$} $line . name d]} {
+ if {!$donebefore} {
+ exec "sh" "-c" $opt(before) <@ stdin >@ stdout 2>@ stderr
+ set donebefore 1
+ }
+ set name "$opt(prefix)$name"
+ set new($name) 1
+ write-safe-file $name "$d\n"
} else {
error "unknown preamble directive"
}
}
if {[info exists o]} {
- puts -nonewline $o $file
close $o
}
close $c
}
write-safe-manifest $conf [array names new]
} {
- exec "sh" "-c" $opt(after)
+ exec "sh" "-c" $opt(after) <@ stdin >@ stdout 2>@ stderr
}
}
}
+
+#----- That's all, folks ----------------------------------------------------
--- /dev/null
+.\" -*-nroff-*-
+.de hP
+.IP
+.ft B
+\h'-\w'\\$1\ 'u'\\$1\ \c
+.ft P
+..
+.ie t .ds o \(bu
+.el .ds o o
+.
+.TH splitconf 1 "9 October 2003" "Straylight/Edgeware"
+.SH NAME
+splitconf \- break a single configuration file out into small chunks
+.
+.SH SYNOPSIS
+.B splitconf
+.B \-u
+.IR config
+.I file
+...
+.br
+.B splitconf
+.B \-d
+.I config
+.br
+.B splitconf
+.RB [ \-s \]
+.I config
+.
+.SH DESCRIPTION
+The
+.B splitconf
+program breaks a configuration file into lots of small files, and
+remembers what they are. It is designed to be
+.IR safe :
+if writing any of the components fails, none of the files is changed.
+Alas, the update is not quite atomic -- doing that involves messing with
+symbolic links and is just too much like hard work.
+.PP
+The command-line arguments are as follows:
+.TP
+.B \-h, \-\-help
+Print a very short help message to standard output and exit.
+.TP
+.B \-u, \-\-unsplit
+Gather a list of
+.IR file s
+into a single
+.BR splitconf -managed
+configuration file
+.IR config .
+.TP
+.B \-d, \-\-delete
+Delete the files split out from
+.IR config .
+.TP
+.B \-s, \-\-split
+Split files out from
+.IR config .
+(This is the default.)
+.
+.SH SPLITCONF FILES
+The input files to
+.B splitconf
+start with a
+.I preamble
+section, followed by a number of
+.I file
+sections.
+.PP
+The preamble section may contain:
+.hP \*o
+blank lines, which are ignored;
+.hP \*o
+meta-comments, which begin with a
+.RB ` ## '
+pair, and are also ignored;
+.hP \*o
+option settings, of the form
+.RI ` option
+.B =
+.IR value '
+(see below); and
+.hP \*o
+one-liner file contents descriptions, of the form
+.RI ` name \c
+.B :
+.IR contents '.
+.PP
+A file section begins with a line of the form
+.RB ` [ \c
+.IR name \c
+.BR ] '
+and is followed by lines to write to the file. In these lines, comments
+(lines beginning with a
+.RB ` # '
+are stripped out, and any trailing blank lines are removed. Also, any
+line beginning with an exclamation mark
+.RB ` ! '
+is written out with the leading exclamation mark removed.
+.PP
+Options are as follows.
+.TP
+.B prefix
+A string to attach to the front of all filenames. (Default is empty.)
+.TP
+.B before
+A shell command to execute before starting to write any files.
+.TP
+.B after
+A shell command to execute after committing the changes or backing out.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding, <mdw@nsict.org>
--- /dev/null
+## Splitconf test script
+
+before = echo "Before!"
+after = echo "After!"
+prefix = test-
+
+one: unus
+two: duo
+three: tres
+
+[four]
+quatuor
+
+[]
+before = echo "Buggy!"
+
+[five]
+cinque
+
+[comments]
+nothing here
+## but us chickens
+who said that?
+!## nobody, in a literal comment
+blank line follows
+
+and then some more text
+# and
+then just one more
+blank line
+!
+
+[]
+prefix =
+
+test.strange: one-liner
+
+[test.odd]
+just some more lines
+like this
--- /dev/null
+.\" -*-nroff-*-
+.TH unfwd 1 "9 October 2003" "Straylight/Edgeware"
+.SH NAME
+unfwd \- extract a MIME-formatted forwarded mesage
+.SH SYNOPSIS
+.B unfwd
+.I program
+.IR args ...
+.SH DESCRIPTION
+Reads a MIME-formatted email message from standard input. For each part
+of content-type
+.BR message/822 ,
+run
+.I program
+and feed the enclosed message to it on its standard input. For each
+part of content-type
+.BR multipart/digest ,
+pick the digest apart and feed each message in turn to
+.I program
+on standard input.
+.PP
+If all went well,
+.B unfwd
+exits 0. If
+.I program
+exits nonzero, or the original message didn't contain any MIME forwarded
+messages, then a message is printed to standard error and
+.B unfwd
+exits 100, which causes
+.B qmail-local
+to bounce the mail. If something else went wrong,
+.B unfwd
+returns 111, indicating a temporary failure.
+.SH BUGS
+None known.
+.SH AUTHOR
+Mark Wooding <mdw@nsict.org>
+