Skip site navigation (1)Skip section navigation (2)
Date:      1 Jul 2006 07:44:26 -0000
From:      nymserv source <nobody@nym.alias.net>
To:        questions@freebsd.org
Subject:   source to the nym.alias.net nymserver
Message-ID:  <20060701074426.29341.qmail@nym.alias.net>

next in thread | raw e-mail | index | archive | help
Some extremely brief instructions on setting up a nym.alias.net style
nym server.  This assumes you already know about remailers and DNS,
and sendmail, and have already setup the appropriate DNS records
(either A or MX) to receive mail at a "nym" domain name on your
machine.

Make sure you have perl5.003 or later, installed with the DB_File
module.  Don't forget to run h2ph when installing perl (cd
/usr/include; h2ph *.h sys/*.h).  If you don't have DB_File, get db
from ftp.cs.berkeley.edu:/ucb/4bsd/db.1.85.tar.gz, install it, and
then reinstall perl so that the DB_File module is available.

Make sure you also have a type 1 remailer running.  By default, the
type1 remailer executable is assumed to be at /usr/remail/remailer.
If you are using the ghio2 remailer, you will need to add the
following lines at the start of the main procedure (also make sure you
define PGPPATH as it will be needed for access to the randseed.bin
file):

  int lockfd;

  umask (007);
  lockfd = open (DIR "/lock", O_CREAT|O_WRONLY, 0777);
  flock (lockfd, LOCK_EX);
#ifdef PGPPATH
  strcpy(input,"PGPPATH=");
  strcat(input,PGPPATH);
  putenv(input);
#endif

Create a new userid/groupid under which to run the nym.  Call it
nymuser.

Create a directory for your nym server to reside in, for example
/usr/nym.  Copy the nymserv perl script into /usr/nym, and make three
subdirectories of /usr/nym:  pgp, queue, and users.  Make sure pgp,
queue, and users are writeable by user nymuser.

Create a PGP key for your nymserver as follows:

   setenv PGPPATH /usr/nym/pgp
   pgp -kg

Put the corresponding PGP passphrase in the file pgp/passphrase.

Create files /usr/nym/users/postmaster.forward and
/usr/nym/users/admin.forward which contain your real E-mail address.
Create a file users/remailer-key.reply which contains the PGP public
key you just created.  Create a ring prototype file.

   echo 'From: nobody@nym.alias.net' > users/remailer-key.reply
   echo 'Subject: PGP key for nym.alias.net' >> users/remailer-key.reply
   echo '' >> users/remailer-key.reply
   pgp +pubring=pgp/pubring.pgp -fkxa >> users/remailer-key.reply

   cp pgp/pubring.pgp ring-proto.pgp

Edit the first few lines of the nymserv executable to set your machine
name and domain name of your nym server.  Make sure you also have
md5sum (if not, get textutils from prep.ai.mit.edu:/pub/gnu).

If you have an A record for your nymserver, set up finger support.
Put a line like this in /etc/inetd.conf:

finger	stream	tcp	nowait	nymuser	/usr/nym/nymserv nymserv -fingerd

Finally, make a new sendmail.cf file which sends all mail to the nym
domain name through the nymserver.  What follows is an example ".mc"
file suitable for use with sendmail 8.7.5.

-------------------------------------------------------------------------------
divert(-1)

# Example sendmail configuration for a nymserver

include(`../m4/cf.m4')

dnl
dnl  Set your Operating system type below
dnl
OSTYPE(solaris2)dnl

dnl
dnl  These flags are necessary to encure privacy (and to prevent nym
dnl  from lines from being rewritten with names of actual users in the
dnl  password file):
dnl
define(`confPRIVACY_FLAGS',
	`novrfy,noexpn,noreceipts,restrictmailq,restrictqrun')dnl
define(`confFROM_HEADER', `$g')dnl
define(`confLOG_LEVEL', `1')dnl
define(`confTO_IDENT', `0s')dnl
define(`HReceived', `H?R?Received')dnl

dnl
dnl  Recommended:
dnl
define(`confMIME_FORMAT_ERRORS', `False')dnl

dnl
dnl  Uncomment the following line if you want procmail used for local
dnl  mail:
dnl
dnl  FEATURE(local_procmail)

PUSHDIVERT(7)dnl

######################################
###   alias mailer specification   ###
######################################

dnl
dnl  Note here that 8888 should be changed to the user ID of the nym
dnl  user, and 9999 should be changed to the group ID of that user.
dnl
Mnym,		P=/usr/nym/nymserv, F=DFMehluS, L=255, T=X-Unix,
		U=8888 9999, S=10/30, R=20/40,
		A=nymserv -d $u

POPDIVERT`'dnl

MAILER(local)dnl
MAILER(smtp)dnl

LOCAL_RULE_0

dnl
dnl  Replace nym.alias.net with the actual name of your nymserver.
dnl
# Redirect the alias mail to the alias mailer
R$+<@nym.alias.net.>	$#nym $: $1
-------------------------------------------------------------------------------

And here is the nymserv perl script:

#!/usr/local/bin/perl -Tw
# $Revision: 1.112 $

#
# nymserv email pseudonym server
# email/finger <source@nym.alias.net> for installation instructions
# email/finger <licence@nym.alias.net> for a copy of the license
# 
# Copyright 1996-1998 David Mazieres
#
# 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 2, 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#

require 5.003;
use strict;

use POSIX qw(:errno_h :fcntl_h);
use DB_File;
use Socket;

require "sys/syscall.ph";

sub LOCK_SH () {0x01;}
sub LOCK_EX () {0x02;}
sub LOCK_NB () {0x04;}
sub LOCK_UN () {0x08;}

# Configuration:

my $HOMEDIR = '/usr/nym';
my $HOSTNAME = 'nym.alias.net';

my $SENDMAIL = '/usr/lib/sendmail';
my $QMAIL_CODES = 0; # Use qmail rather than sendmail exit codes
my $REMAIL = '/usr/remail/remailer';
my $PGP = '/usr/local/bin/pgp';
my $MD5 = '/usr/local/bin/md5sum'; # md5sum from GNU textutils

# When things get bad:
my $CONFIRM = 0;    # Require confirmation of reply-blocks
my $NOCREATE = 0;   # Don't allow creation of new nyms
my $QUOTEREQ = 1;   # Quote autoresponder requests

my $WARNAFTER = 90;
my $DELETEAFTER = 120;

# Stuff you probably don't need to change:
my $PGPPATH = $HOMEDIR . "/pgp";
my $PASSPHRASEFILE = $PGPPATH . "/passphrase";
my $RINGPROTO = "$HOMEDIR/ring-proto.pgp"; # Pubring with just our pub key
my $REPLAY = "$HOMEDIR/replay.db";   # Replay cache
my $CCC = "$HOMEDIR/confirm.db";     # Confirmation cookie cache
my $NDIR = $HOMEDIR . '/users';
my $QDIR = $HOMEDIR . '/queue';
my $QNAM = "q.$$";
my $QPREF = $QDIR . "/" . $QNAM;

my $PGPLOCK = "$QDIR/pgplock";      # Lock for unique access to randseed.bin

my $MAXLINES = 1024;
my $MSGPERDAY = 512;
my $MSGSZUNIT = 32768;
my $MSGSIZE = 10240;
my $SIGDAYS = 7; # Number of days a digital signature is good for

# Flags in user ".dat" file:
my $FL_ENCRECV = 0x1;
my $FL_SIGSEND = 0x2;
my $FL_ACKSEND = 0x100;
my $FL_DISABLED = 0x200;
my $FL_FIXEDSZ = 0x400;
my $FL_FINGERKEY = 0x800;
my $FL_NOBCC = 0x1000;
my $FL_NOLIMIT = 0x10000;
my $DEFFLAGS = 0xfd;

my @RSVD_NAMES = ("config", "send", "list", "root",
		  "nobody", "used", "confirm", "postmaster");

undef %ENV;
$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin';
$ENV{'SHELL'} = '/bin/sh';
$ENV{'IFS'} = ' \t\r';
$ENV{'PGPPATH'} = $PGPPATH;

$SIG{'TERM'} = \&handler;

my $PASSPHRASE;

my $msgmd5;             # MD5 hash of message signature.
my $rmsgid;             # Unique message identifier for received messaged
my $pgppid;             # Process ID of child PGP process

my $lockuser;           # User whose files are locked
my $lockcount = 0;      # Number of times locked

sub fsync (\*) {
    my ($f) = @_;
    select ((select ($f), $|=1)[0]);
    syscall (&SYS_fsync, fileno ($f));
    return 1;
}
sub sync ($) {
    my ($path) = @_;
    local (*F);
    open (F, "<+$path") || return undef;
    fsync (*F);
    close (F);
    return 1;
}

sub wipefile (@) {
    local (*F);
    my ($file, $nblocks, $i);
    my $zeroes = pack "x8192";

    my $sigsave = $SIG{'TERM'};
    $SIG{'TERM'} = 'IGNORE';

    foreach $file (@_) {
	if (open F, "<+$file") {
	    if ($nblocks = (stat F)[7]) {
		$nblocks = ($nblocks + 0x1fff) >> 13;
		for ($i = 0; $i < $nblocks; $i++) {
		    print F $zeroes;
		}
	    }
	    fsync (*F);
	    close F;
	    truncate ("$file", 0);
	}
	unlink $file;
    }

    $SIG{'TERM'} = $sigsave;
}

sub saferename ($$) {
    my ($old, $new) = @_;
    my $tf = "$QPREF.w";
    wipefile ($tf);
    sync ($old);
    my $linked = link $new, $tf;
    my $ret = rename ($old, $new);
    if ($linked) {
	if ($ret) {
	    wipefile ($tf);
	}
	else {
	    unlink ($tf);
	}
    }
    return $ret;
}

sub copyfile (**) {
    my ($in, $out) = @_;
    my ($buf, $len) = ("");

    for (;;) {
	$len = read $in, $buf, 16384;
	($len <= 0) && return $len;
	((print {$out} $buf) < 0) && return -1;
    }
}

sub catfile {
    local (*F);
    my $ret = "";
    if (open F, "<" . $_[0]) {
        while (<F>) {$ret .= $_;}
        close (F);
    }
    else {
	die "$_[0]: $!";
    }
    $ret;
}

my $rndbuf = "";
sub randomval () {
    if (length ($rndbuf) < 4) {
	unlink ("$QPREF.rnd");
	&runpgp ("+makerandom=20 $QPREF.rnd");
	$rndbuf = &catfile ("$QPREF.rnd");
	unlink ("$QPREF.rnd");
    }
    my $val = vec ($rndbuf, 0, 32);
    $rndbuf = substr ($rndbuf, 4);
    $val;
}
sub randomstr () {
    &armor3bytes (&randomval);
}
sub randomfloat {
    my $val = &randomval & 0xffff;
    $val /= 65536.0;
    $val;
}

sub flock_db (\%$) {
    my ($href, $flags) = @_;
    local (*DB_FH);
    my $db = tied %$href;

    $db || fatal (70, "Couldn't get DB object.\n");
    $db->sync;
    open (DB_FH, "+<&" . $db->fd) || die "dup: $!";
    flock (DB_FH, $flags) || fatal (70, "Couldn't flock database: $!.\n");
}
sub tie_lock (\%$;$) {
    my ($href, $path, $flags) = @_;
    my $db;
    local (*DB_FH);

    tie (%$href, "DB_File", "$path", O_CREAT|O_RDWR, 0660, $DB_HASH)
	|| fatal (75, "Couldn't open database: $!.\n");
    $flags = &LOCK_EX unless defined $flags;
    flock_db (%$href, $flags) if $flags;
}
sub untie_unlock (\%) {
    my ($href) = @_;
    flock_db (%$href, &LOCK_UN ());
    untie %$href;
}

sub clean {
    local (*D);
    my $fn;

    opendir (D, "$QDIR") || goto release;
    while (defined ($fn = readdir(D))) {
	if ($fn =~ /^(\Q$QNAM\E.*)/) {
	    unlink "$QDIR/$1";
	}
    }
    closedir (D);

  release:
    if ($lockcount > 0) {
	$lockcount = 1;
	&unlock_user ($lockuser);
    }
}

sub leave {
    my $exitcode = shift;
    &clean;
    if ($QMAIL_CODES) {
	if ($exitcode == 71 || $exitcode == 74 || $exitcode == 75) {
	    $exitcode = 111;
	}
	elsif ($exitcode) {
	    $exitcode = 100;
	}
	    
    }
    exit ($exitcode);
}

sub rollback {
    if ($msgmd5) {
	my %rpc;
	tie_lock (%rpc, $REPLAY);
	delete $rpc{$msgmd5};
	untie_unlock (%rpc);
    }
}

sub handler {
    &rollback;
    kill 15, $pgppid if $pgppid;
    &leave (75);
}

sub fatal {
    printf @_[1..$#_];
    &leave ($_[0]);
}

sub runpgp ($;$$) {
    my ($cmd, $passphrase) = @_;
    my ($ret, $out, $oldflush);
    local (*LF);

    pipe (RPP, WPP) || die "pipe failed" if ($passphrase);
    pipe (ROP, WOP) || die "pipe failed";
    if ($PGPLOCK) {
	open LF, ">$PGPLOCK";
	flock LF, &LOCK_EX;
    }
    $oldflush = $|;        # Flush STDOUT before forking
    $| = 1;
    print "";
    unless ($pgppid = fork) {
	close LF;
	close ROP;
	#print (STDERR "+ $PGP +batchmode +force +verbose=0 $cmd\n");
	open (STDOUT, ">&WOP") || die "couldn't reopen stdout";
	open (STDERR, ">&WOP") || die "couldn't reopen stdout";
	close (STDIN);
	close WOP;
	if ($passphrase) {
	    close WPP;
	    $ENV{'PGPPASSFD'} = fileno RPP;
	}
	unless (exec ("$PGP +batchmode +force +verbose=0 +armorlines=0"
		      . " $cmd")) {
	    print STDERR "Exec of PGP failed.\n";
	    exit (1);
	}
    }
    $| = $oldflush;
    close WOP;
    # Be prepared for PGP to hang
    alarm (120);
    if ($passphrase) {
	close RPP;
	print WPP "$passphrase\n";
	close WPP;
    }
    $out = "";
    while (<ROP>) {
	$out .= $_;
    }
    close (ROP);
    waitpid ($pgppid, 0);
    undef $pgppid;
    alarm (0);
    $ret = $?;
    if ($PGPLOCK) {
	flock LF, &LOCK_UN;
	close LF;
    }
    # printf STDERR "PGP status: 0x%x\n", $ret;
    $_[2] = $out;
    return ($ret>>8);
}

my @BINCHARS = ('-', '0' .. '9', 'A' .. 'Z', '_', 'a' .. 'z');
sub armor3bytes {
    my ($val) = @_;
    my $str = "";
    my $i;
    for ($i = 0; $i < 4; $i++) {
	$str .= $BINCHARS[$val&0x3f];
	$val >>= 6;
    }
    return $str;
}
# lineofgarbage generates text that should be hard to compress, not
# that is cryptographically random.
srand 0;
sub lineofgarbage {
    my ($str, $i) = ("");
    for ($i = 0; $i < 16; $i++) {
	$str .= armor3bytes (int (rand 0x1000000));
    }
    return $str;
}
sub mkmsgid {
    my $tv = pack ("x16");
    syscall (&SYS_gettimeofday, $tv, undef);
    my ($now, $usec) = unpack ("L2", $tv);
    my $id = $BINCHARS[($usec>>6) & 0x3f] . $BINCHARS[$usec & 0x3f];
    foreach ($now, (($now >> 24) & 0x7f) | ($$ << 8)) {
	$id .= &armor3bytes ($_);
    }
    return $id . &randomstr;
}
sub rcvdline {
    my ($user, $plus) = @_;
    my $msgid = &mkmsgid;
    return ("Received: by $HOSTNAME with unique id $msgid for "
	    . "<$user" . ($plus ? $plus : "") . "\@$HOSTNAME>; "
	    . scalar (gmtime) . " +0000 (GMT)\n", $msgid);
}

sub filecost ($) {
    my $file = shift;
    my $size = (stat $file)[7];
    return 1 unless $size;
    return 1 + int ($size/$MSGSZUNIT);
}

my $BGARBAGE = "-----BEGIN GARBAGE-----\n";
my $EGARBAGE = "-----END GARBAGE-----\n";
my $MIMEHDR = <<"EOF";
References: <%s>
Subject: Partial message (part %d of %d)
X-Garbage: %s
Content-type: message/partial; id=\"%s\"; number=%d; total=%d
MIME-Version: 1.0

EOF
sub remail {
    my ($file, $sign, $pubring, $rbfile, $fixedsz) = @_;
    my ($nrbused, $ascfile, $err) = (0);

    if ($pubring) {
	$ascfile = "$file.asc";
	unlink ("$ascfile");
	&runpgp (($sign ?
		  "-seat +pubring=$pubring -u $HOSTNAME" :
		  "-eat +pubring=$pubring +secring=/dev/null")
		 . " $file 0x -o $ascfile", $PASSPHRASE, $err);
    } else {
	$ascfile = "$file";
	$err = "Fatal internal error.\n";
    }
    &fatal (0, "Encrypt/sign file:\n$err")
	unless (-f "$ascfile");

    open (RB, "<$rbfile") || &fatal (0, "Can't open reply block file.\n");
    open (M, "<$ascfile") || &fatal (0, "Missing message file?\n");

    my @breakpos = (0);
    my $padbytes;
    if ($fixedsz) {
	my $len = 0;
	while (<M>) {
	    if ($len + length > $MSGSIZE) {
		fatal (0, "Line too long!\n") if ($len <= 0);
		push @breakpos, $breakpos[$#breakpos] + $len;
		$len = 0;
	    }
	    $len += length;
	}
	$padbytes = $MSGSIZE - $len;
    }
    my $partno = 0;
    my $nparts = @breakpos;
    while (@breakpos) {
	my $line;
	my %rnd;
	my %pr;

	$partno++;
	seek (RB, 0, 0);
	for ($line = <RB>;
	     $line && $line =~ /^Reply-Block:\s*(([a-z])=(0.\d+))?/i;) {
	    my ($set, $val) = ($2, $3);
	  resend:
	    my $pos = $breakpos[0];
	    seek (M, $pos, 0);

	    if ($set) {
		if (!defined ($rnd{$set})) {
		    $rnd{$set} = &randomfloat;
		    $pr{$set} = 0.0;
		}
		if ($pr{$set} < $rnd{$set} && $pr{$set} + $val >= $rnd{$set}) {
		    $nrbused++;
		    open (RM, "| $REMAIL");
		}
		else {
		    open (RM, ">/dev/null");
		}
		$pr{$set} += $val;
	    }
	    else {
		$nrbused++;
		open (RM, "| $REMAIL");
	    }
	    print RM "From nobody\@$HOSTNAME\n\n";
	    while (defined ($line = <RB>) && $line !~ /^Reply-Block:/i) {
		print RM $line;
	    }

	    if ($fixedsz && $nparts > 1) {
		my ($n, $garbage) = (0, "");
		$n = $MSGSIZE - ($breakpos[1] - $breakpos[0])
		    if ($breakpos[1]);
		$n += 2 if ($partno < 10);
		$n += 2 if ($nparts < 10);
		while (length ($garbage) < $n) {
		    $garbage .= substr (&lineofgarbage, 0, $n);
		}
		printf RM ($MIMEHDR, $rmsgid, $partno, $nparts, $garbage,
			   $rmsgid, $partno, $nparts);
	    }
	    while (<M>) {
		print RM $_;
		if ($breakpos[1]) {
		    $pos += length;
		    last if ($pos >= $breakpos[1]);
		}
	    }
	    if ($fixedsz && $partno == $nparts) {
		my $n = $padbytes - length ($BGARBAGE . $EGARBAGE);
		if ($nparts == 1) {
		    $n += length ($MIMEHDR) + 16 - 2;
		}
		if ($n > 0) {
		    print RM $BGARBAGE;
		    while ($n > 65) {
			print RM &lineofgarbage, "\n";
			$n -= 65;
		    }
		    print RM substr (&lineofgarbage, 0, $n - 1), "\n"
			if ($n > 0);
		    print RM $EGARBAGE;
		}
		elsif ($padbytes > 0) {
		    print RM "-" x ($padbytes - 1), "\n";
		}
	    }
	    close (RM) || fatal (0, "Couldn't write to remailer.\n");
	    if ($set && $pr{$set} >= 1.0) {
		$val = $pr{$set} - int ($pr{$set});
		$pr{$set} = 0.0;
		$rnd{$set} = &randomfloat;
		goto resend if ($val > 0.0);
	    }
	}
	shift @breakpos;
    }
    close (RB);
    close (M);
    return $nrbused * ($fixedsz ? $nparts : filecost ($ascfile));
}

sub sendtouser {
    my ($user, $file, $dontsign, $newrb) = @_;
    my $flags = (&read_user_dat ($user))[3];
    my $crypt = ($flags & $FL_ENCRECV);
    my $rb = $newrb ? "$NDIR/$user.nrb" : "$NDIR/$user.rb";

    return &remail ($file, !$dontsign && $crypt,
		    $crypt ? "$NDIR/$user.pgp" : undef,
		    $rb, $flags & $FL_FIXEDSZ)
	if $rb;
    return 0;
}

sub decrypt_stdin {
    my $err;
    open (I, ">$QPREF.i") || &fatal (71, "Error creating queue file ($!).\n");
    while (<STDIN>) { last if /^-----BEGIN PGP MESSAGE-----\s*$/; }
    while ($_) {
	&fatal (71, "Error writing queue file ($!).\n") unless print I $_;
	last if (/^-----END PGP MESSAGE-----\s*$/ || ! ($_ = <STDIN>));
    }
    &fatal (71, "Error writing queue file ($!).\n") unless close (I);

    &runpgp ("-b $QPREF.i -o $QPREF.m", $PASSPHRASE, $err);
    &fatal (66, "Could not decrypt message.\n", $err)
	unless (-f "$QPREF.m");
}

my $TODAY;
sub day_number () {
    $TODAY = int (time / (60 * 60 * 24)) unless defined ($TODAY);
    return ($TODAY);
}

sub lock_user {
    my ($user) = @_;
    if ($lockcount > 0) {
	if ($user ne $lockuser) {
	    &fatal (70, "Attempt to lock multiple users.\n");
	}
	else {
	    $lockcount++;
	    return 1;
	}
    }
    $lockuser = $user;
    do {
	open LOCKFD, ">$NDIR/$user.lock";
	unless (flock (LOCKFD, &LOCK_EX)) {
	    &rollback;
	    &fatal (71, "Can't lock user: $!\n");
	}
    } until ((stat LOCKFD)[3] > 0);
    $lockcount = 1;
    return 1;
}

sub unlock_user {
    my ($user) = @_;
    if (!$lockcount || $lockuser ne $user) {
	&fatal (70, "Attempt to unlock user when not locked.\n");
    }
    if (--$lockcount == 0) {
	flock (LOCKFD, &LOCK_UN);
	if (flock (LOCKFD, &LOCK_EX|&LOCK_NB) && (stat LOCKFD)[3] > 0) {
	    unlink "$NDIR/$user.lock";
	}
	close LOCKFD;
    }
}

sub read_user_dat {
    my $user = lc shift;
    my $today = &day_number;
    my @vals;

    &check_username ($user) || return undef;
    open (DAT, "<$NDIR/$user.dat") || return ($today, 0, $today, $DEFFLAGS);
    $_ = <DAT>;
    unless (@vals = /^(\d+) (\d+) (\d+) (\d+)/) {
	warn "Bad data file $NDIR/$user.dat."; # XXX
	return ($today, 0, $today, $DEFFLAGS);
    }
    $_ = <DAT>;
    chop;
    return (@vals, $_);
}

sub write_user_dat {
    my ($user, $upcount, $upsdate, $flags, $name) = @_;
    my $today = &day_number;
    my $locked;

    &lock_user ($user);
    my @vals = &read_user_dat ($user);
    unless (@vals) {
	&unlock_user ($user);
	return undef;
    }
    if ($today != $vals[0]) {
	$vals[0] = $today;
	$vals[1] = 0;
    }
    if ($upcount > 0) { $vals[1] += $upcount; }
    if ($upcount < 0) { $vals[1] = 0; }
    if ($upsdate) { $vals[2] = $today; }
    if (defined ($flags)) { $vals[3] = $flags; }
    if (defined ($name)) { $vals[4] = $name; }
    my $path = &tmpfile ("$vals[0] $vals[1] $vals[2] $vals[3]\n$vals[4]\n");
    system ("chflags nodump $path");
    sync ($path);
    rename ($path, "$NDIR/$user.dat");
    &unlock_user ($user);
    return $vals[1];
}

sub bump_msg_count {
    my ($user, $inc) = @_;
    write_user_dat ($user, $inc);
    my ($count, $flags) = (read_user_dat ($user))[1,3];
    my $date = gmtime;

    return 1 if ($flags & $FL_NOLIMIT);
    return 1 unless (defined ($count) && $count > $MSGPERDAY);

    $flags |= $FL_DISABLED;
    write_user_dat ($user, 0, 0, $flags);

    &sendtouser ($user, &msgfile ($user, <<"EOF", $count + 1));
From: config\@$HOSTNAME
Date: $date GMT
To: $user\@$HOSTNAME

You have sent sent or received enough mail in one day to exceed the
maximum daily limit.  Your alias account <$user\@$HOSTNAME> has
therefore been disabled.

Please check to make sure this isn\'t a configuration error or a loop
in your reply block.  To reenable your mail alias account, you must
send the following commands, PGP signed and encrypted, to
<config\@$HOSTNAME>:

Config:
From: $user
Nym-Command: -disable
EOF

    return undef;
}

sub parse_address {
    $_ = $_[0];
    /^.*<(\S*)>/ && return $1;
    /^(\S+:)?\s*(\S+)/ && return $2;
    undef;
}

sub reply_hdr {
    my $prio = 0;
    my $sender;
    my $msgid;
    my $hdr;
    my $qhdr = '';

    while (<STDIN>) {
	$qhdr .= "> $_";
	if (/^$/) {
	    last;
	}
	elsif (/^Message-Id:\s*(<\S+>)/) {
	    $msgid = $1;
	}
	elsif (/MAILER-DAEMON/) {
	    return ();
	}
	elsif (/^From\s+(\S+)/ && $prio < 1) {
	    $prio = 1; $sender = $1;
	}
	elsif (/^From:/ && $prio <= 2) {
	    $prio = 2;
	    $sender = &parse_address ($_);
	}
	elsif (/^Reply-To:/) {
	    $prio = 3;
	    $sender = &parse_address ($_);
	}
	elsif (/^X-Loop: $HOSTNAME$/) {
	    return ();
	}
    }
    return () unless ($sender);
    $hdr = "To: $sender\n";
    $hdr .= "References: $msgid\nIn-Reply-To: $msgid\n" if ($msgid);
    $hdr .= "X-Loop: $HOSTNAME\n";
    return ($hdr, $qhdr);
}

sub get_user {
    my ($from) = @_;
    my ($user, $extra);
    ($from =~
     /^From:\s*(\w[\w-]{1,15})(\+[\w-]*)?(@($HOSTNAME)\b[^@]*| [^@]*|)\s*$/i)
	|| return undef;
    $user = $1;
    $extra = $2 ? $2 : "";
    &check_username ($user) || return undef;
    return ($user, $extra);
}

# Calculate some number that grows roughly as the number of days in a
# given date.  This way we can make sure a signature was made at most
# 6-8 days ago (since we will get leap years wrong).
my @dom = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
sub sumtime {
    my ($y, $m, $d) = @_;
    my $i;
    $d += 365 * $y; 
    for ($i = 0; $i < $m; $i++) {
	$d += $dom[$i];
    }
    return ($d);
}

sub check_replay {
    my ($file, $err) = @_;
    my %rpc; # Replay cache

    ($err =~ /^Signature made (\d{4})\/(\d{2})\/(\d{2}) /m) || return (-1);
    my $sigday = &sumtime ($1 - 1900, $2 - 1, $3 + 0);
    my $today = &sumtime ((gmtime)[5,4,3]);
    ($today >= $sigday + $SIGDAYS) && return (1);
    ($sigday > $today + 1)
	&& &fatal (65, "Invalid date on PGP signature\n");

    my $sighash = `$MD5 $file`;
    $sighash =~ s/ .*\n//;

    tie_lock (%rpc, $REPLAY);
    if (defined ($rpc{$sighash})) {
	untie_unlock (%rpc);
	return 1;
    }
    $msgmd5 = $rpc{$sighash};
    $rpc{$sighash} = $sigday;
    $rpc{'.clean'} = 0 unless (defined ($rpc{'.clean'}));
    if ($today > $rpc{'.clean'}) {
	foreach (keys (%rpc)) {
	    delete ($rpc{$_}) if ($today > $rpc{$_} + $SIGDAYS);
	}
	$rpc{'.clean'} = $today;
    }
    untie_unlock (%rpc);
    return undef;
}

sub check_sig {
    my ($pubring, $message) = @_;
    my $err;

    if (&runpgp ("+secring=/dev/null +pubring=$pubring"
		 . " $message.sig $message", undef, $err)
	|| ($err =~
	    /^Good signature from user.*<(config|send)\@$HOSTNAME>/m)) {
	$_[2] = "Invalid PGP signature.\n";
	return undef;
    }
    if (&check_replay ("$message.sig", $err)) {
	&fatal (0, "Discarding replay or old message.\n");
#	$_[2] = "Message replay or invalid date on PGP signature.\n";
#	return undef;
    }
    return 1;
}

sub check_username {
    my ($user) = lc shift;

    if ($user !~ /^\w[\w-]{1,15}$/) {
	$_[1] = "Invalid alias name.\n";
	return undef;
    }
    if (-f "$NDIR/$user.forward" || -f "$NDIR/$user.reply") {
	$_[1] = "Alias $user\@$HOSTNAME is reserved.\n";
	return undef;
    }
    foreach (@RSVD_NAMES) {
	$_ = lc $_;
	if ($user eq $_) {
	    $_[1] = "Alias $user\@$HOSTNAME is reserved.\n";
	    return undef;
	}
    }
    return (1);
}

sub authorize_user {
    my ($user) = @_;
    my $err;

    unless (&check_username ($user , $err)) {
	$_[1] = $err;
	return undef;
    }
    unless (&check_sig ("$NDIR/$user.pgp", "$QPREF.m", $err)) {
	$_[1] = $err;
	return undef;
    }
    &write_user_dat ($user, 0, 1);
    return 1;
}

my $tcnt = 0;
sub tmpfile {
    local *TMP;
    my $path = "$QPREF.t" . $tcnt++;
    unlink ("$path");
    open (TMP, ">$path") || &fatal (70, "Could not create temporary file.\n");
    unless ((printf TMP @_) && (close (TMP))) {
	&fatal (74, "Could not write temporary file.\n");
    }
    return ($path);
}
sub msgfile {
    my ($user, @msg) = @_;
    my ($recvd, $msgid) = &rcvdline ($user);
    return &tmpfile ("%s$msg[0]", $recvd, (@msg[1..$#msg]));
}

sub ccc_delete (\%$) {
    my ($href, $user) = @_;
    my $cookie;
    if ($cookie = $$href{$user}) {
	$cookie =~ s/^\S+\s+//;
	delete ($$href{$cookie});
	delete ($$href{$user});
	return 1;
    }
    return undef;
}
sub ccc_cookie (\%$) {
    my ($href, $user) = @_;
    my $cookie;
    if ($cookie = $$href{$user}) {
	$cookie =~ s/^\S+\s+//;
	return $cookie;
    }
    return undef;
}
sub ccc_new (\%$) {
    my ($href, $user) = @_;
    my $cookie = sprintf "+%08x%08x", randomval, randomval;
    my $today = &day_number;
    ccc_delete (%$href, $user);
    $$href{$user} = "$today $cookie";
    $$href{$cookie} = "$today $user";
    return $cookie;
}
sub ccc_clean () {
    my ($key, $val, $day, $u);
    my $date = gmtime;
    my $today = day_number;
    my %ccc;
    my @goners;
    my @killednrb;

    tie_lock (%ccc, $CCC);
    foreach $key (keys %ccc) {
	($day, $val) = split /\s+/, $ccc{$key};
	next unless defined ($day) && defined ($val);
	next unless $today > $day + $SIGDAYS;
	delete $ccc{$key};
	if ($key !~ /\+/) {
	    if (-f "$NDIR/$key.rb") {
		wipefile ("$NDIR/$key.nrb");
		push @killednrb, $key;
	    }
	    else {
		push @goners, $key;
	    }
	}
    }
    $ccc{'.clean'} = $today;
    untie_unlock (%ccc);

    my $user;
    foreach $user (@goners) {
	lock_user ($user);
	wipefile ("$NDIR/$user.dat", "$NDIR/$user.nrb",
		  "$NDIR/$user.pgp") unless -f "$NDIR/$user.rb";
	unlock_user ($user);
    }
    foreach $user (@killednrb) {
	&sendtouser ($user, &msgfile ($user, <<"EOF"));
From: confirm\@$HOSTNAME
Date: $date GMT
To: $user\@$HOSTNAME

You submitted a new reply block for your pseudonym a while ago.
However, you never confirmed that reply block.  This is most likely
because the new reply block did not work properly and you never
received confirmation instructions.  However, it may also be because a
message got lost in transit.

In either event, if you still want to update your reply block you
should send a new copy to config\@$HOSTNAME.
EOF
    }
}

sub runconfig {
    my ($user, $pubkey, $rblock, $create, $delete);
    my $err = "";
    my $cmds = "";

    &decrypt_stdin;

    open (M, "<$QPREF.m")
	|| &fatal (66, "Could not decrypt message.\n");
    while (<M>) {
	last if (/^Config:/);
	/^$/ || &fatal (0, "Discarding non-config message.\n");
    }
    $_ || &fatal (0, "Discarding empty input message.\n");
  hdrloop:
    while (<M>) {
	if ($. > $MAXLINES) {
	    $err .= "Header exceeds maximum number of lines.\n";
	    last;
	}
	elsif (/^From:/) {
	    if (defined ($user)) {
		$err .= "Duplicate From: line\n";
	    }
	    else {
		$err .= "Message contains CR characters.  "
		    . "Did you forget the -t flag to PGP?\n" if /\r\n/;
		my $extra;
		($user, $extra) = &get_user ($_);
		$user || ($err .= "Illegal/reserved user name or invalid "
			  . "machine name in From: line.\n");
		$extra && ($err .= "Can't send config requests from "
			   . "\"plussed\" addresses\n");
		$user =~ tr/A-Z/a-z/ if ($user);
	    }
	}
	elsif (/^Public-Key:/i) {
	    if (defined ($pubkey)) {
		$err .= "duplicate Public-Key: line\n";
	    } else {
		$pubkey = "";
		while (<M>) {
		    $pubkey .= $_;
		    last if /^-----END/;
		    if ($. > $MAXLINES) { undef ($pubkey); redo hdrloop; }
		}
	    }
	}
	elsif (/^Reply-Block:/i) {
	    my $blank = 1;
	    $rblock = $_;
	    while (<M>) {
		if ($. > $MAXLINES) { undef ($rblock); redo hdrloop; }
		s/\r$//;
		if (/^Reply-Block:/i && !$blank) { $rblock .= "\n"; }
		$rblock .= $_;
		$blank = /^$/;
	    }
	    $rblock .= "\n" unless ($blank);
	}
	elsif (/^Nym-Commands?:(.*)/i) {
	    $cmds .= "$1\n";
	}
	elsif (/^([\w-]+:)/) {
	    $err .= "Unknown header \"$1\".\n";
	}
    }
    close (M);

    my ($rbfile, $pubring, $pgperr, $flags);

    if ($rblock) {
	$rbfile = "$QPREF.rb";
	open (RB, ">$rbfile")
	    || &fatal (70, "Can't open reply block file ($!)\n");
	&fatal (74, "Error updating reply block ($!).\n")
	    unless ((print RB $rblock) && close (RB));
	system ("chflags nodump $QPREF.rb");
    }
    if ($pubkey) {
	$pubring = "$QPREF.pgp";
	system ("cp $RINGPROTO $pubring");
	open (ASC, ">$QPREF.asc")
	    || &fatal (70, "Can't open public key file ($!)\n");
	select ((select (ASC), $| = 1)[$[]);
	&fatal (74, "Error updating public key file ($!).\n")
	    unless ((print ASC $pubkey) && close (ASC));
	if (&runpgp ("-ka +secring=/dev/null"
		     . " +pubring=$pubring"
		     . " $QPREF.asc", undef, $pgperr)
	    || $pgperr !~ /^\s*1 new key/m) {
	    $err .= "Error setting new public key:$pgperr\n";
	    undef ($pubring);
	}
	else {
	    chmod (0660, "$QPREF.pgp");
	    system ("chflags nodump $QPREF.pgp");
	}
    }

    my $incctr = 0;
    my $fullname;

    $user && &lock_user ($user);

    if ($user && $cmds) {
	my $rest = $cmds;
	$flags = (&read_user_dat ($user))[3];
	while ($rest =~ /^\s*([\w+-]+[=?]?)/) {
	    $_ = $1;
	    $rest = $';
	    if (/^(\+|-)(\w+)$/) {
		my $val;
		if ($2 eq "acksend") {
		    $val = $FL_ACKSEND;
		}
		elsif ($2 eq "cryptrecv") {
		    $val = $FL_ENCRECV;
		}
		elsif ($2 eq "signsend") {
		    $val = $FL_SIGSEND;
		}
		elsif ($2 eq "disable") {
		    $val = $FL_DISABLED;
		    #$incctr = -1 if ($1 eq "-");
		}
		elsif ($2 eq "fixedsize") {
		    $val = $FL_FIXEDSZ;
		}
		elsif ($2 eq "fingerkey") {
		    $val = $FL_FINGERKEY;
		}
		elsif ($2 eq "nobcc") {
		    $val = $FL_NOBCC;
		}
		else {
		    $err .= "Invalid Nym-Command switch `$1' in `$1$2'.\n";
		    next;
		}
		if ($1 eq "+") {
		    $flags |= $val;
		}
		else {
		    $flags &= ~$val;
		}
	    }
	    elsif ($_ eq "name=") {
		unless ($rest =~ /^\"(([^\\\"$;\n]|\\\\|\\\")*)\"\s/) {
		    $err .= "Could not parse your name=\"...\" Nym-Command.\n";
		    $rest = "";
		    last;
		}
		$rest = $';
		$fullname = $1;
		$fullname =~ s/\\\"/\"/g;
		$fullname =~ s/\\\\/\\/g;

		$err .= "Full names cannot contain unprintable characters.\n"
		    if $fullname =~ /[\x00-\x1f\x7f-\x9f\xff]/;

		my $s = $fullname;
		$s =~ s/\\./../g;
		$s =~ s/\"[^\"]*\"//g;
		$err .= <<"EOF" if $s =~ /[@\",<>]/;
Full names cannot have unbalanced '\"' characters or unquoted
    '<', '>', '\@', or ',' characters in name= Nym-Commands.
EOF

                $err .= "Full name is too long.\n"
		    if length ($fullname) > 255;
	    }
	    elsif ($_ eq "delete") {
		$delete = 1;
	    }
	    elsif ($_ eq "create") {
		$create = 1;
	    }
	    elsif ($_ eq "create?") {
		$create = 1 unless -f "$NDIR/$user.pgp";
	    }
	    else {
		$err .= "Invalid Nym-Command `$_'.\n";
		$rest =~ s/^\"(([^\\\"$;\n]|\\\\|\\\")*)\"\s//;
	    }
	}
	if ($rest =~ /^\s*(\S[^\n\r]*)/) {
	    $err .= "Could not parse Nym-Command `$1'.\n";
	}
    }

    $err .= "$HOSTNAME is not currently granting new aliases.\n"
	if ($create && $NOCREATE);

    my $authorized;

    if ($user) {
	if (! $create) {
	    if (-f "$NDIR/$user.pgp") {
		($authorized = &authorize_user ($user, $pgperr))
		    || ($err .= "$pgperr ");
	    }
	    else {
		$err = "No such user.  Use \"Nym-Command: create\" to "
		    . "create a new alias.\n";
	    }
	}
	else {
	    $err .= "Can't use delete Nym-Command with create.\n"
		if ($delete);
	    $rbfile || ($err .= "You must specify at least one reply-block"
			. " with \"Reply-Block\".\n");
	    if ($pubring) {
		&check_sig ($pubring, "$QPREF.m", $pgperr)
		    || ($err .= "$pgperr  (when checking against the public"
			. "key in your message)\n");
	    }
	    else {
		$err .= "You must specify a PGP public key with"
		    . " \"Public-Key:\".\n";
	    }
	    if ($err || -f "$NDIR/$user.pgp") {
		$err .= "The username you chose is already in use.\n"
		    unless ($err);
	    }
	}
    }
    else {
	$err .= "No/bad From: line designating Nym.\n";
    }

    my $date = gmtime;

    if ($err) {
	$user && &unlock_user ($user);
	my $file = &msgfile ($user ? $user : "UNKNOWN ALIAS", <<"EOF",
From: config\@$HOSTNAME
Date: $date GMT
To: %s%s

Your request to modify or create %s could
not be performed.  The following error(s) were encountered:

%s
EOF
			     $user ? "$user\@$HOSTNAME"
			     : "UNKNOWN ALIAS",
			     $authorized ? "" : " (unauthentic)",
			     $user ? "alias <$user\@$HOSTNAME>"
			     : "an unspecified alias", $err);
	if ($authorized && !$rbfile) {
	    &sendtouser ($user, $file);
	}
	elsif ($rbfile) {
	    &remail ($file, $authorized,
		     $authorized ? "$NDIR/$user.pgp" : $pubring,
		     $rbfile);
	}
	&fatal (0, $err);
    }

    my %ccc;

    if ($delete) {
	my $usenrb = "";
	&fatal (0, "Can't delete when not authorized!\n")
	    unless ($authorized);
	$SIG{'TERM'} = 'IGNORE';
	unlink ("$QPREF.pgp");
	if ($CONFIRM) {
	    tie_lock (%ccc, $CCC);
	    ccc_delete (%ccc, $user);
	    untie_unlock (%ccc);
	    $usenrb = "n" if (-f "$NDIR/$user.nrb");
	}
	unless (-f "$QPREF.rb") {
	    rename (("$NDIR/$user." . $usenrb . "rb"), "$QPREF.rb")
		|| &fatal (0, "rename .rb file failed\n");
	}
	wipefile ("$NDIR/$user.rb", "$NDIR/$user.nrb", "$NDIR/$user.dat");
	rename ("$NDIR/$user.pgp", "$QPREF.pgp")
	    || &fatal (0, "rename .pgp file failed\n");
	&unlock_user ($user);

	&remail (&msgfile ($user, <<"EOF"), 1, "$QPREF.pgp", "$QPREF.rb");
From: config\@$HOSTNAME
Date: $date GMT
To: $user\@$HOSTNAME

This message is to confirm that pseudonym <$user\@$HOSTNAME>
was deleted on $date GMT.  Your reply-block
and PGP key will be wiped once this message is mailed.
EOF

	wipefile ("$QPREF.rb", "$QPREF.pgp");
	&leave (0);
    }

    my ($cmsg, $cookie) = ("");
    tie_lock (%ccc, $CCC) if ($CONFIRM);
    $SIG{'TERM'} = 'IGNORE';
    if ($pubring) {
	saferename ("$pubring", "$NDIR/$user.pgp");
    }
    if ($rbfile) {
	if ($CONFIRM) {
	    ccc_new (%ccc, $user);
	    saferename ("$rbfile", "$NDIR/$user.nrb");
	}
	else {
	    saferename ("$rbfile", "$NDIR/$user.rb");
	}
    }
    &write_user_dat ($user, $incctr, 1, $flags, $fullname);
    &unlock_user ($user);
    my $replyto = '';
    if ($CONFIRM && ($cookie = ccc_cookie (%ccc, $user))) {
	$replyto = "Reply-To: confirm$cookie\@$HOSTNAME\n";
	$cmsg = <<"EOF";

A new reply block has been received for your mail alias, but has not
yet been activated.  In order to start receiving mail with your new
reply block, you must confirm it by sending an (anonymous) E-mail
message to the following address:

   confirm$cookie\@$HOSTNAME

The contents of the message can be anything.  Any message delivered to
this address will activate your reply block.

EOF
    }
    my $doclean = ($CONFIRM && !($ccc{'.clean'}
				 && $ccc{'.clean'} == &day_number));
    untie_unlock (%ccc) if ($CONFIRM);
    &sendtouser ($user,
		 &msgfile ($user, $replyto . <<"EOF" . $cmsg), undef,
From: config\@$HOSTNAME
Date: $date GMT
To: $user\@$HOSTNAME

Your configuration request completed successfully.
EOF
		 defined ($cookie));
    $SIG{'TERM'} = \&handler;
    # Here would be a good place to receive signals.
    $SIG{'TERM'} = 'IGNORE';
    ccc_clean if ($doclean);
    &leave (0);
}

sub runconfirm {
    my $arg = lc shift;
    my ($user, $cookie);
    my $date = gmtime;
    my %ccc;

    (-f $CCC) || fatal (67, "Invalid username\n");
    tie_lock (%ccc, $CCC, 0);
    ($user = $ccc{$arg}) || &leave (0);
    $user =~ s/^\S+\s+//;
    lock_user ($user);
    flock_db (%ccc, &LOCK_EX ());
    delete ($ccc{"c:$arg"});
    (($cookie = ccc_cookie (%ccc, $user)) && $cookie eq $arg)
	|| &leave (0);
    $SIG{'TERM'} = 'IGNORE';
    ccc_delete (%ccc, $user);
    (-f "$NDIR/$user.nrb")
	|| &fatal (70, "No reply block for valid confirmation\n");
    saferename ("$NDIR/$user.nrb", "$NDIR/$user.rb")
	|| &fatal (70, "Cannot install new reply block\n");
    flock_db (%ccc, &LOCK_UN ());
    unlock_user ($user);
    &sendtouser ($user, &msgfile ($user, <<"EOF"));
From: confirm\@$HOSTNAME
Date: $date GMT
To: $user\@$HOSTNAME

Your new reply block has been confirmed and installed.  Your mail
alias is currently active.
EOF

    &leave (0);
}

sub runsend {
    my $user;
    my $date = gmtime;
    my $inhdr;
    my $flags;
    my $hiddento = '';
    my $headerto = '';
    my $resentto = '';
    my $nymcommands = '';
    my $warnings = '';
    my $hasbody;
    my $notmailed = '';

    &decrypt_stdin;

    open (H, ">$QPREF.h");
    open (B, ">$QPREF.b");
    open (M, "<$QPREF.m")
	|| &fatal (66, "Could not decrypt message.\n");
    while (<M>) {
	unless ($inhdr) {
	    /^$/ && next;
	    /^Config:/i && &fatal (0, "Discarding config message to send.\n");
	    $inhdr = 1;
	    /^::$/ && next;
	}
      moreheaders:
	if ($. > $MAXLINES) {
	    &fatal (70, "Header exceeds maximum number of lines.\n");
	}
	elsif (/^From:/) {
	    my $line = $_;
	    my ($name, $extra);

	    &fatal (0, "duplicate From: line\n") if defined ($user);
	    ($user, $extra) = &get_user ($_);
	    $user || &fatal (0, "Illegal user name or invalid "
			     . "machine name in From: line.\n");
	    ($flags, $name) = (&read_user_dat ($user))[3, 4];
	    if ($name) {
		$line = "From: $name <$user$extra\@$HOSTNAME>\n";
	    }
	    else {
		$line = "From: $user$extra\@$HOSTNAME\n";
	    }
	    (print H $line)
		|| &fatal (71, "Error writing queue file ($!).\n");
	    $user =~ tr/A-Z/a-z/;
	    next;
	}
	elsif (/^Hidden-To:(.*)$/i) {
	    $hiddento .= " $1";
	    while (<M>) {
		if (/^[ \t]/) {chomp ($hiddento .= $_);}
		else {goto moreheaders;}
	    }
	}
	elsif (/^(Resent-)?(To|Cc|Bcc):(.*)$/i) {
	    my $rl = $1 ? \$resentto : \$headerto;
	    $$rl .= " $3";
	    (print H $_)
		|| &fatal (71, "Error writing queue file ($!).\n");
	    while (<M>) {
		if (/^[ \t]/) {chomp ($$rl .= $_);}
		else {goto moreheaders;}
		(print H $_)
		    || &fatal (71, "Error writing queue file ($!).\n");
	    }
	}
	elsif (/^Nym-Commands?:(.*)$/i) {
	    $nymcommands .= " $1";
	    while (<M>) {
		if (/^[ \t]/) {chomp ($nymcommands .= $_);}
		else {goto moreheaders;}
	    }
	}
	if (/^$/) {
	    undef $_;
	    last;
	}
	elsif (/^\S/ && !/^\S+:/) {
	    print B $_;
	    $hasbody = 1;
	    undef $_;
	    last;
	}
	(print H $_)
	    || &fatal (71, "Error writing queue file ($!).\n");
    }
    print H "\n" unless defined $_;
    &fatal (65, "No From: line.\n") unless $user;
    foreach (split ' ', $nymcommands) {
	next unless $_;
	/^\+acksend/ && ($flags |= $FL_ACKSEND, next);
	/^\-acksend/ && ($flags &= ~$FL_ACKSEND, next);
	/^\+signsend/ && ($flags |= $FL_SIGSEND, next);
	/^\-signsend/ && ($flags &= ~$FL_SIGSEND, next);
	$warnings .= "Ignored unknown nym-command `$_' (non-fatal).\n";
    }
    close (H) || &fatal (71, "Error writing queue file ($!).\n");
    while (<M>) {
	$hasbody = 1 if !$hasbody && /./;
	print B $_
	    || &fatal (71, "Error writing queue file ($!).\n");
	if ($hasbody) {
	    copyfile (*M, *B);
	    last;
	}
    }
    close (M);

    print B <<"EOF" if ($flags & $FL_SIGSEND && $hasbody);

~~~
This PGP signature only certifies the sender and date of the message.
It implies no approval from the administrators of $HOSTNAME.
Date: $date GMT
From: $user\@$HOSTNAME
EOF
    close (B) || &fatal (71, "Error writing queue file ($!).\n");

    &authorize_user ($user)
	|| &fatal (0, "Invalid PGP signature on message\n");

    if ($CONFIRM && ! -f "$NDIR/$user.rb" && -f "$NDIR/$user.nrb") {
	my %ccc;
	my $cookie;

	lock_user ($user);
	tie_lock (%ccc, $CCC, &LOCK_SH ());
	$cookie = ccc_cookie (%ccc, $user);
	untie_unlock (%ccc);
	if (! -f "$NDIR/$user.rb" && -f "$NDIR/$user.nrb" && $cookie) {
	    &sendtouser ($user, &msgfile ($user, <<"EOF"), undef, 1);
Reply-To: confirm$cookie\@$HOSTNAME
From: send\@$HOSTNAME
Date: $date GMT
To: $user\@$HOSTNAME

You have attempted to send a message through send\@$HOSTNAME.
However, the message could not be resent because your alias does not
yet have an active reply block.  To activate your reply block and make
your alias functional, you must send E-mail (anonymously) to this
address:

   confirm$cookie\@$HOSTNAME

The contents of the mail you send is not important.  Any message
delivered to this address will activate your reply block.

EOF
	    &fatal (0, "Reply block not activated.\n");
	}
	unlock_user ($user);
    }

    if ($flags & $FL_DISABLED) {
	$notmailed = 'NOT ';
	$warnings .= "Your nym account is disabled.\n"
	    . "Send -disable Nym-Command to reenable.\n";
    }

    my $result;
    my @recips;

    my $err;
    if (!$notmailed && ($flags & $FL_SIGSEND) && $hasbody) {
	&fatal (78, "Could not create PGP signature.\n$err")
	    if (&runpgp ("-sat $QPREF.b", $PASSPHRASE, $err)
		|| ! -f "$QPREF.b.asc");
    }
    else {
	rename "$QPREF.b", "$QPREF.b.asc";
    }

    open (H, "<$QPREF.h");
    open (B, "<$QPREF.b.asc");

    if ($hiddento) {
	@recips = grep {$_} split /[\s,]+/, $hiddento;;
	unless (@recips) {
	    $notmailed = 'NOT ';
	    $warnings .= "No valid recipients in Hidden-To: header.\n";
	}
    }
    if ($notmailed) {
	open (SEND, ">/dev/null");
    }
    elsif ($hiddento) {
	open (SEND, "|-")
	    || exec ($SENDMAIL, "-f", "$user\@$HOSTNAME",
		     "-os", "-oem", "-oi", "--", (@recips))
		|| &fatal (1, "Couldn't run $SENDMAIL @recips\n");
    }
    else {
	open (SEND, "| $SENDMAIL -f $user\@$HOSTNAME -os -oem -oi -t");
    }
    copyfile (*H, *SEND);
    copyfile (*B, *SEND);

    $result = close (SEND);
    unless ($notmailed) {
	my ($nrecips, $to) = (1);
	($to = $hiddento) || ($to = $resentto) || ($to = $headerto);
	$to =~ s/\([^\)]*\)//;
	if ($to) {
	    my @rl = split /[\s,]+/, $to;
	    $nrecips = @rl;
	}
	bump_msg_count ($user, $nrecips, filecost ("$QPREF.b.asc"));
    }
    undef $msgmd5;

    if ($result && ($flags & $FL_ACKSEND)
	|| $hiddento && !@recips || $warnings || $notmailed) {
	seek H, 0, 0;
	open ACK, ">$QPREF.a";
	print ACK (&rcvdline ($user))[0];
	print ACK <<"EOF";
From: send\@$HOSTNAME
Date: $date GMT
To: $user\@$HOSTNAME

EOF
   	if ($hiddento) {
	    if (@recips) {
		print ACK <<"EOF", "\t", join (",\n\t", @recips), "\n";
A message with the following header was ${notmailed}remailed to these
recipients under your pseudonym:
EOF
	    }
	    else {
		print ACK <<"EOF";
The message you sent with the following header could not be remailed
because no valid recipients were found in the 'Hidden-To:' header.
EOF
	    }
        } 
	else {
	    print ACK <<"EOF";
This is to acknowledge that a message with the following header was
${notmailed}remailed under your pseudonym:
EOF
	}
	print ACK "\n";
	while (<H>) { print ACK $_; }
	print ACK "\nSome problems were encountered with your "
	    . "message:\n\n", $warnings if ($warnings);
	close (ACK);
	&sendtouser ($user, "$QPREF.a");
    }

    close (H);
    close (B);
    &leave (0);
}

sub runreceive {
    my ($user, $extra) = @_;
    $extra = "" unless (defined ($extra));
    my $date = gmtime;
    my $flags = (&read_user_dat ($user))[3];
    my ($recip, $rrecip) = ('', '');

    if ($flags & $FL_DISABLED) {
	write_user_dat ($user, 1);
	&fatal (69, "Account disabled.\n");
    }

    open (I, ">$QPREF.i") || &fatal (71, "Error creating queue file ($!).\n");
    my $rcvd;
    $extra = '' unless defined $extra;
    ($rcvd, $rmsgid) = &rcvdline ($user, $extra);
    print I $rcvd;
    while (<STDIN>) {
      restart:
	s/^From /X-From: /;
	&fatal (71, "Error writing queue file ($!).\n") unless print I $_;
	last if /^(\s*|[^\s:]+(\s.*)?)$/;
	if (/^(Resent-)?(To|Cc):(.*)$/i) {
	    my $rp = $1 ? \$rrecip : \$recip;
	    $$rp .= $3;
	    while (<STDIN>) {
		goto restart unless /^\s/;
		&fatal (71, "Error writing queue file ($!).\n")
		    unless print I $_;
		$$rp .= $_;
	    }
	}
    }

    if (!$extra && ($flags & $FL_NOBCC)) {
	my $erecip = "$user$extra\@$HOSTNAME";
	$recip = $rrecip if $rrecip;
	unless ($recip =~ /\b\Q$erecip\E\b/i) {
	    &fatal (77, <<"EOF");
This user does not wish to receive blind carbon copies.  You must
specify the user\'s full E-mail address in a To or Cc mail header.
EOF
	}
    }

    copyfile (*STDIN, *I);
    print I <<"EOF" if ($flags & $FL_ENCRECV);

~~~
This PGP signature only certifies the receipt and date of the message.
It implies no approval from the administrators of $HOSTNAME.
Date: $date GMT
To: $user\@$HOSTNAME
EOF
    close (I) || &fatal (71, "Error writing queue file ($!).\n");

    my $nmsg = &sendtouser ($user, "$QPREF.i", !($flags & $FL_ENCRECV));
    bump_msg_count ($user, $nmsg);

    &leave (0);
}

sub usage {
    my $prog = $0;
    $prog =~ s/.*\///;
    fatal (64, <<"EOF");
Usage: $prog -d user
       $prog -fingerd
       $prog -wipe user ...
       $prog -expire [-f]
EOF
}

sub usednyms () {
    opendir (D, $NDIR) || return ();
    my $last = '';
    my @list = grep ({($last ne $_, ($last = $_))[0]}
		     (sort @RSVD_NAMES,
		      grep (s/^(.*)\.(pgp|forward|reply)$/$1/, readdir D)));
    closedir (D);
    return @list;
}

sub rundeliver {
    chop ($PASSPHRASE = catfile ($PASSPHRASEFILE));
    &clean;
    fatal (64, "Usage: nymserv -d recipient\n") unless (@ARGV == 1);
    fatal (67, "Invalid username\n")
	unless ($ARGV[0] =~ /^(\w[\w-]{1,15})(\+[\w-]*)?$/);
    my $recip = lc $1;
    my $plussed = $2;

    if ($recip eq "nobody") {
	&leave (0);
    }
    elsif ($recip eq "config") {
	&runconfig;
    }
    elsif ($recip eq "send") {
	&runsend;
    }
    elsif ($recip eq "confirm" && $plussed) {
	&runconfirm ($plussed);
    }
    elsif ($recip =~ /^(list|used)$/) {
	my ($H, $Q) = &reply_hdr;
	&leave (65, "No valid return address found\n") unless $H;
	my @list = &usednyms;
	open SM, "| $SENDMAIL -oi -f nobody\@$HOSTNAME -t";
	print SM $H;
	print SM "Subject: list of used nyms on $HOSTNAME\n";
	print SM "From: used nyms <nobody\@$HOSTNAME>\n\n";
	print SM join ("\n", @list), "\n";
	print SM "\n", $Q if ($QUOTEREQ);
	&leave (0);
    }
    elsif (-r "$NDIR/$recip.forward") {
	my $inb;
	my $to;
	chop ($to = `cat $NDIR/$recip.forward`);
	($to =~ /^(.*)$/) && ($to = $1);
	if ($to =~ /^\|(.*)/) {
	    exec $1 || &fatal (72, "Exec of $1 failed\n");
	}
	open SM, "| $SENDMAIL -oi -f nobody\@$HOSTNAME -- $to";
	while (<STDIN>) {
	    unless ($inb) {
		s/^From /X-From: /;
		s/^(Return-Receipt-To:)/X-$1/i;
		s/^(Notice-Requested-Upon-Delivery-To:)/X-$1/i;
		s/^(Errors-To:)/X-$1/i;
		$inb = 1 if (/^$/);
	    }
	    print SM $_;
	}
	close (SM);
	&leave (0);
    }
    elsif (-f "$NDIR/$recip.reply") {
	my ($H, $Q) = &reply_hdr;
	&leave (65, "No valid return address found\n") unless $H;
	open SM, "| $SENDMAIL -oi -f nobody\@$HOSTNAME -t";
	print SM $H;
	open F, "<$NDIR/$recip.reply";
	while (<F>) {
	    print SM $_;
	}
	print SM "\n", $Q if ($QUOTEREQ);
	close (F);
	close (SM);
	&leave (0);
    }
    elsif (-r "$NDIR/$recip.pgp") {
	(-r "$NDIR/$recip.rb") || &fatal (69, "Account disabled.\n");
	&runreceive ($recip, $plussed);
    }
    else {
	fatal (67, "Invalid username\n");
    }
    &leave (0);
}

sub runfingerd {
    my ($target, $warning, $fingerdir);
    my $BLURB = ucfirst <<"EOF";
$HOSTNAME offers untraceable E-mail pseudonyms.  Finger
help\@$HOSTNAME for more information about this service,
or visit http://www.cs.berkeley.edu/~raph/n.a.n.html.
EOF

    $SIG{'ALRM'} = sub {&fatal (1, "fingerd timed out\n");};
    alarm (30);

    $target = <STDIN>;
    if ($target) {
	$target =~ s/\r?\n//;
    } else {
	$target = '';
    }

    if ($target =~ /(.*)@([^@]+)/) {
	my ($user,$host,$port,$ipaddr,$sin,$myaddr) = ($1, $2);
	$host .= "." unless $host =~ /\d{1,3}(\.\d{1,3}){1,3}/;
	&fatal (1, "unknown host: %s\n", $host)
	    unless ($ipaddr = inet_aton ($host));
	&fatal (1, "internal error: unknown service finger\n")
	    unless ($port = getservbyname ('finger', 'tcp'));
	socket (SOCK, PF_INET, SOCK_STREAM, getprotobyname ('tcp'))
	    || &fatal (1, "internal error: could not create socket\n");
	$sin = sockaddr_in ($port, $ipaddr);
	printf "[finger %s@%s]\n", $user, $host;
	$myaddr = (unpack_sockaddr_in (getsockname (STDIN)))[1];
	bind (SOCK, sockaddr_in (0, $myaddr))
	    || &fatal (1, "internal error: can't bind socket\n");
	&fatal (1, "Connection refused while connecting to %s\n", $host)
	    if ($ipaddr eq INADDR_LOOPBACK);
	connect (SOCK, $sin)
	    || &fatal (1, "$! while connecting to %s\n", $host);
	select ((select (SOCK), $| = 1)[$[]);
	printf SOCK "%s\r\n", $user;
	while (<SOCK>) {
	    print;
	}
	exit (0);
    }

    $target =~ tr/A-Z/a-z/;
    $target =~ s/^\/w//;		# RFC 742 is weird

    if ($target =~ /^([\w][\w-]{0,15})$/) {
	$target = $1;
    }
    else {
	if ($target) {
	    print "finger: $target: no such user.\n";
	}
	else {
	    print $BLURB;
	    print <<"EOF";

In addition to fingering individual E-mail aliases, you can finger:
    remailer-key - Public key for the $HOSTNAME nym server.
    help         - Help file for the $HOSTNAME nym server.
    list         - List of taken pseudonyms.

EOF
			}
	exit;
    }

    $target = 'remailer-key' if ($target eq 'config' || $target eq 'send');

    if ($target =~ /^(list|used)$/) {
	unless (chdir ($NDIR)) {
	    print "No information on $target.\n";
	    exit;
	}
	print "List of pseudonyms in use on $HOSTNAME:\n\n";
	print join("\n", &usednyms), "\n";
    }
    elsif (-r "$NDIR/$target.reply") {
	unless (open (DATA, "<$NDIR/$target.reply")) {
	    print "No information on $target.\n";
	    exit;
	}
	while (<DATA>) {
	    last unless /^\S+:/;
	}
	print if $_ && !/^$/;
	print while (<DATA>);
    }
    elsif (-r "$NDIR/$target.forward") {
	printf STDOUT ("Mail Alias:  %-24s  Name:  %s\n", $target, '???');
	print "\n", $BLURB;
    }
    elsif (-r "$NDIR/$target.dat") {
	my ($flags, $fullname) = (&read_user_dat ($target))[3,4];
	printf STDOUT ("Mail Alias:  %-24s  Name:  %s\n", $target,
		       ($fullname && $fullname =~ /\S/) ? $fullname : "???");
	if ($flags & $FL_FINGERKEY) {
	    my $key;
	    &runpgp ("-fkxa '<$target\@$HOSTNAME>' $NDIR/$target.pgp"
		     . " 2> /dev/null", undef, $key);
	    print "PGP Public-Key:\n$key"
		if $key =~ /^-----BEGIN PGP PUBLIC KEY BLOCK-----/;
	}
	print "\n", $BLURB;
    }
    else {
	print "finger: $target: no such user.\n";
    }
    &leave (0);
}

sub runwipe {
    my $user;
    my %ccc;
    tie_lock (%ccc, $CCC, 0);
    foreach $user (@_) {
	unless ($user =~ /^(\w[\w-]*)$/) {
	    warn "bad user: $user";
	    next;
	}
	$user = $1;
	lock_user ($user);
	flock_db (%ccc, &LOCK_EX ());
	ccc_delete (%ccc, $user);
	wipefile ("$NDIR/$user.rb", "$NDIR/$user.nrb", "$NDIR/$user.dat",
		  "$NDIR/$user.pgp");
	flock_db (%ccc, &LOCK_UN ());
	unlock_user ($user);
    }
    untie_unlock (%ccc);
}

sub keyinfo ($;$) {
    my ($ring, $user) = @_;
    my ($result, $bits, $id, $fingerprint);
    $user = "0x" unless defined $user;
    &runpgp ("-kvc $user $ring", undef, $result);
    $result || return ();
    ($bits, $id) = ($result =~ /^pub\s+(\d+)\/(\w+)/m);
    ($fingerprint) = ($result =~ /^\s+Key fingerprint\s*=\s*(\S.*)$/m);
    return ($bits, $id, $fingerprint);
}

sub runexpire {
    my $force = shift;

    $force = undef unless (defined ($force) && $force eq '-f');

    my ($nym, $days);

    my @warn;
    my @expire;

    &day_number;

    opendir N, "$NDIR";
    foreach $nym (grep {/\.dat$/} readdir (N)) {
	$nym =~ s/\.dat$//;
	$days = $TODAY - (&read_user_dat ($nym))[2];
	if ($days > $DELETEAFTER) {
	    push @expire, $nym;
	}
	elsif ($days > $WARNAFTER) {
	    push @warn, $nym;
	}
    }
    closedir N;

    if ($force) {
	&runwipe (@expire);

	foreach $nym (@warn) {
	    my $keyid = (keyinfo ("$NDIR/$nym.pgp"))[1];
	    $keyid = $keyid ? "0x$keyid" : 'yournym_PGP_key_ID';
	    open SM, "|$SENDMAIL -f nobody\@$HOSTNAME -t";
	    print SM <<"EOF";
From: Alias expiration daemon <nobody\@$HOSTNAME>
To: $nym\@$HOSTNAME
Subject: Your mail alias is expiring

In order to clean up abandoned mail aliases or aliases with lost PGP
keys, a daemon periodically deletes pseudonyms which appear to be
unused.  It has been over $WARNAFTER days since the last time
$HOSTNAME received a piece of E-mail signed by your private key.  Your
pseudonym is therefore considered inactive, and will be deleted if it
is still inactive after $DELETEAFTER days.

If you do not wish to have your mail alias deleted, simply send any
piece of E-mail through <send\@$HOSTNAME>, or any configuration
message to <config\@$HOSTNAME>--your pseudonym will then automatically
be renewed.  This may also be a good time update your reply block with
a new list of remailers and Encrypt-Keys, as the list of reliable
remailers can change from month to month and any configuration message
will renew your pseudonym.

Note that receiving mail through your pseudonym does not renew it, as
an abandoned account could still be receiving mail.  In order to renew
your account, you must show the nym server that someone still has
access to your account\'s private PGP key.  Note further that just
changing the mail header of your outgoing mail to show your $HOSTNAME
address will not renew your pseudonym either.  (This is insecure
anyway and so is not recommended in the first place.)

The simplest way to renew your pseudonym is to create a file called,
for instance, renew, with the following contents (indented one space
here for clarity, do not indent it when you create the file):

 Config:
 From: $nym

Then sign and encrypt this message with this pgp command:

 pgp -seat renew config\@$HOSTNAME -u $keyid

Here $keyid corresponds to the hex key ID of the PGP key under
which you created your pseudonym account.  (You can also use the
descriptive name of your PGP key instead of the hex key ID.  If you
are unsure what you named your PGP key, you can check the entire
contents of your keyring with the command pgp -kv.)

The above pgp command will create a file called renew.asc.  You must
then mail the contents of that file to <config\@$HOSTNAME>, either
directly, or, preferably, through some anonymous remailers.  When the
nym server receives your message, it will send you a confirmation
saying 'Your configuration request completed successfully.'  Once you
receive this confirmation, your alias account will have been renewed.

Note:  You must create a new renew.asc file each time you renew your
pseudonym.  Nym.alias.net has a replay cache which prevents an
intercepted message from being replayed multiple times by an attacker.
Thus, each signed renew.asc file will only work once, and will only
work if the date in the PGP signature is current.

If you have any questions about this policy or other problems with
$HOSTNAME, please raise them on the newsgroup alt.privacy.anon-server,
or contact admin\@$HOSTNAME as a last resort.

EOF
            close SM;
	    sleep 2;
	}
    }

    if (@warn || @expire) {
	print "Pseudonym expiration report:\n";
	print "warned: @warn\n\n" if (@warn);
	print "expired: @expire\n" if (@expire);
    }
}

umask (007);
$( = (split /\s+/, $), 2)[0];
$< = $>;
chdir ($HOMEDIR) || die "$HOMEDIR: $!";
$SIG{'ALRM'} = \&handler;

my $flag = shift;
if (!$flag) { &usage; }
elsif ($flag eq '-d') { &rundeliver (@ARGV); }
elsif ($flag eq '-fingerd') { &runfingerd; }
elsif ($flag eq '-wipe') { &runwipe (@ARGV); }
elsif ($flag eq '-expire') { &runexpire (@ARGV); }
else { &usage; }
&leave (0);


> From questions@freebsd.org Sat Jul 01 07:44:26 2006
> Message-ID: <20060701074426.1187.qmail@nym.alias.net>
> Received: from freebsd.org (81.52.169.218 [81.52.169.218])
>     by nym.alias.net with SMTP;
>     for source@nym.alias.net;
>     Sat, 01 Jul 2006 03:44:25 -0400 (EDT)
>     (envelope-from questions@freebsd.org)
> Received-SPF: SoftFail; receiver=nym.alias.net; client-ip=81.52.169.218;
>     envelope-from=<questions@freebsd.org>; helo=freebsd.org; mechanism=~all
> X-Avenger: version=0.7.6; receiver=nym.alias.net; client-ip=81.52.169.218;
>     client-port=3747; syn-fingerprint=65535:111:1:48:M1460,N,N,S Windows 2000
>     SP4, XP SP1; data-bytes=0; network-path=18.26.0.1 128.30.0.245 18.4.7.1
>     18.168.0.27 38.112.2.213 66.250.14.205 130.117.0.45 130.117.1.122
>     130.117.1.50 0.0.0.0 193.251.128.118 193.251.240.38 193.251.241.45
>     193.251.250.194 81.52.165.2; network-path-time=1151739864;
>     RBL=cbl.abuseat.org (127.0.0.2)
> From: questions@freebsd.org
> To: source@nym.alias.net
> Subject: Lwtfylzmn
> Date: Sat, 1 Jul 2006 08:43:36 +0200
> MIME-Version: 1.0
> Content-Type: multipart/mixed;
> 	boundary="----=_NextPart_000_0001_2BD1B279.4C5B165D"
> X-Priority: 3
> X-MSMail-Priority: Normal
> X-Mailer: Microsoft Outlook Express 6.00.2600.0000
> X-MIMEOLE: Produced By Microsoft MimeOLE V6.00.2600.0000
> 



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20060701074426.29341.qmail>