From e86e7d0e07c7c5ae4f14802275bdc28593c4a395 Mon Sep 17 00:00:00 2001 From: Poul-Henning Kamp Date: Tue, 31 Jan 1995 19:12:53 +0000 Subject: [PATCH] CTM email tools. Reviewed by: phk Submitted by: Stephen McKay --- usr.sbin/ctm/Makefile | 2 +- usr.sbin/ctm/Makefile.inc | 5 + usr.sbin/ctm/ctm_rmail/Makefile | 6 + usr.sbin/ctm/ctm_rmail/ctm_rmail.1 | 310 ++++++++++++++++++ usr.sbin/ctm/ctm_rmail/ctm_rmail.c | 486 +++++++++++++++++++++++++++++ usr.sbin/ctm/ctm_rmail/error.c | 70 +++++ usr.sbin/ctm/ctm_rmail/error.h | 3 + usr.sbin/ctm/ctm_rmail/options.h | 139 +++++++++ usr.sbin/ctm/ctm_smail/Makefile | 7 + usr.sbin/ctm/ctm_smail/ctm_smail.c | 324 +++++++++++++++++++ 10 files changed, 1351 insertions(+), 1 deletion(-) create mode 100644 usr.sbin/ctm/Makefile.inc create mode 100644 usr.sbin/ctm/ctm_rmail/Makefile create mode 100644 usr.sbin/ctm/ctm_rmail/ctm_rmail.1 create mode 100644 usr.sbin/ctm/ctm_rmail/ctm_rmail.c create mode 100644 usr.sbin/ctm/ctm_rmail/error.c create mode 100644 usr.sbin/ctm/ctm_rmail/error.h create mode 100644 usr.sbin/ctm/ctm_rmail/options.h create mode 100644 usr.sbin/ctm/ctm_smail/Makefile create mode 100644 usr.sbin/ctm/ctm_smail/ctm_smail.c diff --git a/usr.sbin/ctm/Makefile b/usr.sbin/ctm/Makefile index dd9a76685df8..5987a968c9fc 100644 --- a/usr.sbin/ctm/Makefile +++ b/usr.sbin/ctm/Makefile @@ -1,4 +1,4 @@ -SUBDIR= ctm ctm_scan +SUBDIR= ctm ctm_scan ctm_rmail ctm_smail .include diff --git a/usr.sbin/ctm/Makefile.inc b/usr.sbin/ctm/Makefile.inc new file mode 100644 index 000000000000..2ecf88423934 --- /dev/null +++ b/usr.sbin/ctm/Makefile.inc @@ -0,0 +1,5 @@ +# $Id$ + +.if exists(${.CURDIR}/../../Makefile.inc) +.include "${.CURDIR}/../../Makefile.inc" +.endif diff --git a/usr.sbin/ctm/ctm_rmail/Makefile b/usr.sbin/ctm/ctm_rmail/Makefile new file mode 100644 index 000000000000..274a79d3c81f --- /dev/null +++ b/usr.sbin/ctm/ctm_rmail/Makefile @@ -0,0 +1,6 @@ +PROG= ctm_rmail +SRCS= ctm_rmail.c error.c +CFLAGS+= -Wall -g +MLINKS+= ctm_rmail.1 ctm_smail.1 + +.include diff --git a/usr.sbin/ctm/ctm_rmail/ctm_rmail.1 b/usr.sbin/ctm/ctm_rmail/ctm_rmail.1 new file mode 100644 index 000000000000..a6dbc646ef9d --- /dev/null +++ b/usr.sbin/ctm/ctm_rmail/ctm_rmail.1 @@ -0,0 +1,310 @@ +.\" NOTICE: This is free documentation. I hope you get some use from these +.\" words. In return you should think about all the nice people who sweat +.\" blood to document their free software. Maybe you should write some +.\" documentation and give it away. Maybe with a free program attached! +.\" +.\" Author: Stephen McKay +.\" +.Dd January 15, 1995 +.Os +.Dt CTM_MAIL 1 +.Sh NAME +.Nm ctm_smail, ctm_rmail +.Nd send and receive +.Nm ctm +deltas via mail +.Sh SYNOPSIS +.Nm ctm_smail +.Op Fl l Ar log +.Op Fl m Ar maxmsgsize +.Op Fl c Ar maxctmsize +.Ar ctm-delta +.Ar mail-alias +.Nm ctm_rmail +.Op Fl D +.Op Fl l Ar log +.Op Fl p Ar piecedir +.Op Fl d Ar deltadir +.Op Fl b Ar basedir +.Op Ar +.Sh DESCRIPTION +In conjuction with the +.Xr ctm 1 +command, +.Nm ctm_smail +and +.Nm ctm_rmail +are used to distribute changes to a source tree via email. +.Nm ctm_smail +is given a compressed +.Nm ctm +delta, and a mailing list to send it to. It splits the delta into manageable +pieces, encodes them as mail messages and sends them to the mailing list. +Each recipient uses +.Nm ctm_rmail +(either manually or automatically) to decode and reassemble the delta, and +optionally call +.Xr ctm 1 +to apply it to the source tree. +At the moment, +only two source trees are distributed, and both by the same site. These are +the FreeBSD-current source and CVS trees, distributed by +.Li ref.tfs.com . +.Pp +Command line arguments for +.Nm ctm_smail : +.Bl -tag -width indent +.It Fl l Ar log +Instead of appearing on +.Em stderr , +error diagnostics and informational messages (other than command line errors) +are time stamped and written to the file +.Em log . +.It Fl m Ar maxmsgsize +Limit the maximum size mail message that +.Nm ctm_smail +is allowed to send. It is approximate since mail headers and other niceties +are not counted in this limit. If not specified, it will default to 64000 +bytes, leaving room for 1535 bytes of headers before the rumoured 64k mail +limit. +.It Fl c Ar maxctmsize +Limit the maximum size delta that will be sent. Deltas bigger that this +limit will cause an apology mail message to be sent to the mailing list. +This is to prevent massive changes overwhelming users' mail boxes. Note that +this is the size before encoding. Encoding causes a 4/3 size increase before +mail headers are added. If not specified, there is no limit. +.El +.Pp +.Ar ctm-delta +is the delta to be sent, and +.Ar mail-alias +is the mailing list to send the delta to. +The mail messages are sent using +.Xr sendmail 8 . +.Pp +Command line arguments for +.Nm ctm_rmail : +.Bl -tag -width indent +.It Fl l Ar log +Instead of appearing on +.Em stderr , +error diagnostics and informational messages (other than command line errors) +are time stamped and written to the file +.Em log . +.It Fl p Ar piecedir +Collect pieces of deltas in this directory. Each piece corresponds to a +single mail message. Pieces are removed when complete deltas are built. +If this flag is not given, no input files will be read, but completed +deltas may still be applied with +.Xr ctm 1 +if the +.Fl b +flag is given. +.It Fl d Ar deltadir +Collect completed deltas in this directory. Deltas are built from one or +more pieces when all pieces are present. +.It Fl b Ar basedir +Apply any completed deltas to this source tree. If this flag is not given, +deltas will be stored, but not applied. The user may then apply the deltas +manually, or by using +.Nm ctm_rmail +without the +.Fl p +flag. +Deltas will not be applied if they do not match the +.Li .ctm_status +file in +.Ar basedir +(or if +.Li .ctm_status +does not exist). +.It Fl D +Delete deltas after successful application by +.Xr ctm 1 . +It is probably a good idea to avoid this flag (and keep all the deltas) +as one of the possible future enhancements to +.Xr ctm 1 +is the ability to recover small groups of files from a full set of deltas. +.El +.Pp +The file arguments (or +.Em stdin , +if there are none) are scanned for delta pieces. Multiple delta pieces +can be read from a single file, so an entire maildrop can be scanned +and processed with a single command. +.Sh FILE FORMAT +Following are the important parts of an actual (very small) delta piece: +.Bd -literal +From: src-cur-owner +To: src-cur +Subject: ctm-mail src-cur.0003.gz 1/4 + +CTM_MAIL BEGIN src-cur.0003.gz 1 4 +H4sIAAAAAAACA3VU72/bNhD9bP0VByQoEiyRSZEUSQP9kKTeYCR2gDTdsGFAwB/HRogtG5K8NCj6 +v4+UZSdtUQh6Rz0eee/xaF/dzx8up3/MFlDkBNrGnbttAwyo1pxoRgoiBNX/QJ5d3c9/X8DcPGGo +lggkPiXngE4W1gUjKPJCYyk5MZRbIqmNW/ASglIFcdwIzTUxaAqhnCPcBqloKEkJVNDMF0Azk+Bo +dDzzk0Ods/+A5gXv9YyJHjMCtJwQNeESNma7hOmXDRxn +CTM_MAIL END 61065 +.Ed +.Pp +The subject of the message always begins with +.Dq ctm-mail +followed by the name of the delta, which piece this is, and how many total +pieces there are. The data is bracketed by +.Dq CTM_MAIL BEGIN +and +.Dq CTM_MAIL END +lines, duplicating the information in the subject line, plus a simple checksum. +.Pp +If the delta exceeds +.Ar maxctmsize , +then a message like this will be received instead: +.Bd -literal +From: src-cur-owner +To: src-cur +Subject: ctm-notice src-cur.0999.gz + +src-cur.0999.gz is 792843 bytes. The limit is 300000 bytes. + +You can retrieve this delta via ftpmail, or your good mate at the university. +.Ed +.Pp +You are then on your own! +.Sh EXAMPLES +To send delta 32 of +.Em src-cur +to a group of wonderful code hackers known to +.Xr sendmail 8 +as +.Em src-guys , +limiting the mail size to roughly 60000 bytes, you could use: +.Bd -literal -offset indent +ctm_smail -m 60000 /wherever/it/is/src-cur.0032.gz src-guys +.Ed +.Pp +To decode every +.Nm ctm-mail +message in your mailbox, assemble them into complete deltas, then apply +any deltas built or lying around, you could use: +.Bd -literal -offset indent +ctm_rmail -p ~/pieces -d ~/deltas -b /usr/ctm-src-cur $MAIL +.Ed +.Pp +(Note that no messages are deleted by +.Nm ctm_rmail . +Any mail reader could be used for that purpose.) +.Pp +To create a mail alias called +.Em receiver-dude +that will automatically decode and assemble deltas, but not apply them, +you could put the following lines in your +.Pa /etc/aliases +file (assuming the +.Pa /ctm/tmp +and +.Pa /ctm/deltas +directories and +.Pa /ctm/log +file are writable by user +.Em daemon +or group +.Em wheel ) : +.Bd -literal -offset indent +receiver-dude: "|ctm_rmail -p /ctm/tmp -d /ctm/deltas -l /ctm/log" +owner-receiver-dude: real_dude@wherever.you.like +.Ed +.Pp +The second line will catch failures and drop them into your regular mailbox, +or wherever else you like. +.Pp +To apply all the deltas collected, and delete those applied, you could use: +.Bd -literal -offset indent +ctm_rmail -d /ctm/deltas -b /ctm/src-cur -l /ctm/apply.log +.Ed +.Sh SECURITY +If you automatically take your mail and pass it to a file tree patcher, you +might think you are handing the keys to your system to the hackers! Happily, +the window for mischief is quite small. +.Nm ctm_rmail +is careful to write only to the directories given to it (by not believing any +.Dq / +characters in the delta name), and the latest +.Nm ctm +disallows absolute pathnames in files it manipulates, so the worst you +could lose are a few source tree files (recoverable from your deltas). +Since +.Nm ctm +requires that a +.Nm md5 +checksum match before it touches a file, only fellow +source recipients would be able to generate a fake delta, and they're such +nice folk that they wouldn't even think of it! :-) +.Pp +Even this possibility could be removed by using cryptographic signatures. +A possible future enhancement would be to use +.Nm PGP +to provide a secure wrapper. +.\" This next request is for sections 1, 6, 7 & 8 only +.Sh ENVIRONMENT +If deltas are to be applied then +.Xr ctm 1 +and +.Xr gunzip 1 +must be in your +.Ev PATH . +.Sh FILES +.Bl -tag -width indent +.It Pa PIECEDIR/* +Pieces of deltas waiting for the rest. +.It Pa DELTADIR/* +Completed deltas. +.It Pa BASEDIR/.ctm_status +File containing name and number of the next delta to be applied to this +source tree. +.\" This next request is for sections 1, 6, 7 & 8 only +.\" (command return values (to shell) and fprintf/stderr type diagnostics) +.Sh DIAGNOSTICS +.Nm ctm_smail +and +.Nm ctm_rmail +return exit status 0 for success, and 1 for various failures. +.Nm ctm_rmail +is expected to be called from a mail transfer program, and thus signals +failure only when the input mail message should be bounced (preferably into +your regular maildrop, not back to the sender). In short, failure to +apply a completed delta with +.Nm ctm +is not considered an error important enough to bounce the mail, and +.Nm ctm_rmail +returns an exit status of 0. +.Pp +In normal operation, +.Nm ctm_smail +will report messages like: +.Bd -literal -offset indent +ctm_smail: src-cur.0250.gz 1/2 sent to src-guys +.Ed +.Pp +.Nm ctm_rmail +will report messages like: +.Bd -literal -offset indent +ctm_rmail: src-cur.0250.gz 1/2 stored +ctm_rmail: src-cur.0250.gz 2/2 stored +ctm_rmail: src-cur.0250.gz complete +.Ed +.Pp +These messages go to +.Em stderr +or to the log file. Messages from +.Nm ctm +turn up here too. Error messages should be self explanatory. +.\" The next request is for sections 2 and 3 error and signal handling only. +.\" .Sh ERRORS +.Sh SEE ALSO +.Xr ctm 1 +(coming soon) +.\" .Sh STANDARDS +.\" .Sh HISTORY +.Sh AUTHOR +Stephen McKay +.\" .Sh BUGS diff --git a/usr.sbin/ctm/ctm_rmail/ctm_rmail.c b/usr.sbin/ctm/ctm_rmail/ctm_rmail.c new file mode 100644 index 000000000000..859b0edf1138 --- /dev/null +++ b/usr.sbin/ctm/ctm_rmail/ctm_rmail.c @@ -0,0 +1,486 @@ +/* + * Accept one (or more) ASCII encoded chunks that together make a compressed + * CTM delta. Decode them and reconstruct the deltas. Any completed + * deltas may be passed to ctm for unpacking. + * + * Author: Stephen McKay + * + * NOTICE: This is free software. I hope you get some use from this program. + * In return you should think about all the nice people who give away software. + * Maybe you should write some free software too. + */ + +#include +#include +#include +#include +#include +#include +#include +#include "error.h" +#include "options.h" + +#define CTM_STATUS ".ctm_status" + +char *piece_dir = NULL; /* Where to store pieces of deltas. */ +char *delta_dir = NULL; /* Where to store completed deltas. */ +char *base_dir = NULL; /* The tree to apply deltas to. */ +int delete_after = 0; /* Delete deltas after ctm applies them. */ + +void apply_complete(void); +int read_piece(char *input_file); +int combine_if_complete(char *delta, int pce, int npieces); +int decode_line(char *line, char *out_buf); + +/* + * If given a '-p' flag, read encoded delta pieces from stdin or file + * arguments, decode them and assemble any completed deltas. If given + * a '-b' flag, pass any completed deltas to 'ctm' for application to + * the source tree. The '-d' flag is mandatory, but either of '-p' or + * '-b' can be omitted. If given the '-l' flag, notes and errors will + * be timestamped and written to the given file. + * + * Exit status is 0 for success or 1 for indigestible input. That is, + * 0 means the encode input pieces were decoded and stored, and 1 means + * some input was discarded. If a delta fails to apply, this won't be + * reflected in the exit status. In this case, the delta is left in + * 'deltadir'. + */ +main(int argc, char **argv) + { + char *log_file = NULL; + int status = 0; + + err_prog_name(argv[0]); + + OPTIONS("[-D] [-p piecedir] [-d deltadir] [-b basedir] [-l log] [file ...]") + FLAG('D', delete_after) + STRING('p', piece_dir) + STRING('d', delta_dir) + STRING('b', base_dir) + STRING('l', log_file) + ENDOPTS + + if (delta_dir == NULL || piece_dir == NULL && (base_dir == NULL || argc>1)) + usage(); + + if (log_file != NULL) + err_set_log(log_file); + + if (argc <= 1) + { + if (piece_dir != NULL) + status = read_piece(NULL); + } + else + { + while (*++argv != NULL) + status |= read_piece(*argv); + } + + if (base_dir != NULL) + apply_complete(); + + return status; + } + + +/* + * Construct the file name of a piece of a delta. + */ +#define mk_piece_name(fn,d,p,n) \ + sprintf((fn), "%s/%s+%d-%d", piece_dir, (d), (p), (n)) + +/* + * Construct the file name of an assembled delta. + */ +#define mk_delta_name(fn,d) \ + sprintf((fn), "%s/%s", delta_dir, (d)) + +/* + * If the next required delta is now present, let ctm lunch on it and any + * contiguous deltas. + */ +void +apply_complete() + { + int i, dn; + FILE *fp, *ctm; + struct stat sb; + char class[20]; + char delta[30]; + char fname[1000]; + char buf[2000]; + char junk[2]; + char here[1000]; + + sprintf(fname, "%s/%s", base_dir, CTM_STATUS); + if ((fp = fopen(fname, "r")) == NULL) + return; + + i = fscanf(fp, "%s %d %c", class, &dn, junk); + fclose(fp); + if (i != 2) + return; + + /* + * We might need to convert the delta filename to an absolute pathname. + */ + here[0] = '\0'; + if (delta_dir[0] != '/') + { + getcwd(here, sizeof(here)-1); + i = strlen(here) - 1; + if (i >= 0 && here[i] != '/') + { + here[++i] = '/'; + here[++i] = '\0'; + } + } + + /* + * Keep applying deltas until we run out or something bad happens. + */ + for (;;) + { + sprintf(delta, "%s.%04d.gz", class, ++dn); + mk_delta_name(fname, delta); + + if (stat(fname, &sb) < 0) + return; + + sprintf(buf, "(cd %s && ctm %s%s) 2>&1", base_dir, here, fname); + if ((ctm = popen(buf, "r")) == NULL) + { + err("ctm failed to apply %s", delta); + return; + } + + while (fgets(buf, sizeof(buf), ctm) != NULL) + { + i = strlen(buf) - 1; + if (i >= 0 && buf[i] == '\n') + buf[i] = '\0'; + err("ctm: %s", buf); + } + + if (pclose(ctm) != 0) + { + err("ctm failed to apply %s", delta); + return; + } + + if (delete_after) + unlink(fname); + + err("%s applied%s", delta, delete_after ? " and deleted" : ""); + } + } + + +/* + * This cheap plastic checksum effectively rotates our checksum-so-far + * left one, then adds the character. We only want 16 bits of it, and + * don't care what happens to the rest. It ain't much, but it's small. + */ +#define add_ck(sum,x) \ + ((sum) += ((x)&0xff) + (sum) + (((sum)&0x8000) ? 1 : 0)) + + +/* + * Decode the data between BEGIN and END, and stash it in the staging area. + * Multiple pieces can be present in a single file, bracketed by BEGIN/END. + * If we have all pieces of a delta, combine them. Returns 0 on success, + * and 1 for any sort of failure. + */ +int +read_piece(char *input_file) + { + int status = 0; + FILE *ifp, *ofp; + int decoding = 0; + int line_no = 0; + int i, n; + int pce, npieces; + unsigned claimed_cksum; + unsigned short cksum; + char out_buf[200]; + char line[200]; + char delta[30]; + char pname[1000]; + char junk[2]; + + ifp = stdin; + if (input_file != NULL && (ifp = fopen(input_file, "r")) == NULL) + { + err("cannot open '%s' for reading", input_file); + return 1; + } + + while (fgets(line, sizeof(line), ifp) != NULL) + { + line_no++; + + /* + * Look for the beginning of an encoded piece. + */ + if (!decoding) + { + if (sscanf(line, "CTM_MAIL BEGIN %s %d %d %c", delta, &pce, &npieces, junk) == 3) + { + char *s; + + while ((s = strchr(delta, '/')) != NULL) + *s = '_'; + + mk_piece_name(pname, delta, pce, npieces); + if ((ofp = fopen(pname, "w")) == NULL) + { + err("cannot open '%s' for writing", pname); + status++; + continue; + } + + cksum = 0xffff; + decoding++; + } + continue; + } + + /* + * We are decoding. Stop if we see the end flag. + */ + if (sscanf(line, "CTM_MAIL END %d %c", &claimed_cksum, junk) == 1) + { + int e; + + decoding = 0; + + fflush(ofp); + e = ferror(ofp); + fclose(ofp); + + if (e) + err("error writing %s", pname); + + if (cksum != claimed_cksum) + err("checksum: read %d, calculated %d", claimed_cksum, cksum); + + if (e || cksum != claimed_cksum) + { + err("%s %d/%d discarded", delta, pce, npieces); + unlink(pname); + status++; + continue; + } + + err("%s %d/%d stored", delta, pce, npieces); + + if (!combine_if_complete(delta, pce, npieces)) + status++; + continue; + } + + /* + * Must be a line of encoded data. Decode it, sum it, and save it. + */ + n = decode_line(line, out_buf); + if (n < 0) + { + err("line %d: illegal character: '%c'", line_no, line[-n]); + err("%s %d/%d discarded", delta, pce, npieces); + + fclose(ofp); + unlink(pname); + + status++; + decoding = 0; + continue; + } + + for (i = 0; i < n; i++) + add_ck(cksum, out_buf[i]); + + fwrite(out_buf, sizeof(char), n, ofp); + } + + if (decoding) + { + err("truncated file"); + err("%s %d/%d discarded", delta, pce, npieces); + + fclose(ofp); + unlink(pname); + + status++; + } + + if (ferror(ifp)) + { + err("error reading %s", input_file == NULL ? "stdin" : input_file); + status++; + } + + if (input_file != NULL) + fclose(ifp); + + return (status != 0); + } + + +/* + * Put the pieces together to form a delta, if they are all present. + * Returns 1 on success (even if we didn't do anything), and 0 on failure. + */ +int +combine_if_complete(char *delta, int pce, int npieces) + { + int i; + FILE *dfp, *pfp; + int c; + struct stat sb; + char pname[1000]; + char dname[1000]; + + /* + * All here? + */ + for (i = 1; i <= npieces; i++) + { + if (i == pce) + continue; + mk_piece_name(pname, delta, i, npieces); + if (stat(pname, &sb) < 0) + return 1; + } + + mk_delta_name(dname, delta); + + /* + * We can probably just rename() it in to place if it is a small delta. + */ + if (npieces == 1) + { + mk_piece_name(pname, delta, 1, 1); + if (rename(pname, dname) == 0) + { + err("%s complete", delta); + return 1; + } + } + + if ((dfp = fopen(dname, "w")) == NULL) + { + err("cannot open '%s' for writing", dname); + return 0; + } + + /* + * Ok, the hard way. Reconstruct the delta by reading each piece in order. + */ + for (i = 1; i <= npieces; i++) + { + mk_piece_name(pname, delta, i, npieces); + if ((pfp = fopen(pname, "r")) == NULL) + { + err("cannot open '%s' for reading", pname); + fclose(dfp); + unlink(dname); + return 0; + } + while ((c = getc(pfp)) != EOF) + putc(c, dfp); + fclose(pfp); + } + fflush(dfp); + if (ferror(dfp)) + { + err("error writing '%s'", dname); + fclose(dfp); + unlink(dname); + return 0; + } + fclose(dfp); + + /* + * Throw the pieces away. + */ + for (i = 1; i <= npieces; i++) + { + mk_piece_name(pname, delta, i, npieces); + unlink(pname); + } + + err("%s complete", delta); + return 1; + } + + +/* + * MIME BASE64 decode table. + */ +static unsigned char from_b64[0x80] = + { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, + 0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff + }; + + +/* + * Decode a line of ASCII into binary. Returns the number of bytes in + * the output buffer, or < 0 on indigestable input. Error output is + * the negative of the index of the inedible character. + */ +int +decode_line(char *line, char *out_buf) + { + unsigned char *ip = (unsigned char *)line; + unsigned char *op = (unsigned char *)out_buf; + unsigned long bits; + unsigned x; + + for (;;) + { + if (*ip >= 0x80 || (x = from_b64[*ip]) >= 0x40) + break; + bits = x << 18; + ip++; + if (*ip < 0x80 && (x = from_b64[*ip]) < 0x40) + { + bits |= x << 12; + *op++ = bits >> 16; + ip++; + if (*ip < 0x80 && (x = from_b64[*ip]) < 0x40) + { + bits |= x << 6; + *op++ = bits >> 8; + ip++; + if (*ip < 0x80 && (x = from_b64[*ip]) < 0x40) + { + bits |= x; + *op++ = bits; + ip++; + } + } + } + } + + if (*ip == '\0' || *ip == '\n') + return op - (unsigned char *)out_buf; + else + return -(ip - (unsigned char *)line); + } diff --git a/usr.sbin/ctm/ctm_rmail/error.c b/usr.sbin/ctm/ctm_rmail/error.c new file mode 100644 index 000000000000..be3581d4aaed --- /dev/null +++ b/usr.sbin/ctm/ctm_rmail/error.c @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include "error.h" + +static FILE *error_fp = NULL; +static char *prog = NULL; + + +/* + * Log errors to the given file. + */ +void +err_set_log(char *log_file) + { + FILE *fp; + + if ((fp = fopen(log_file, "a")) == NULL) + err("cannot log to '%s'", log_file); + else + error_fp = fp; + } + + +/* + * Set the error prefix if not logging to a file. + */ +void +err_prog_name(char *name) + { + if ((prog = strrchr(name, '/')) == NULL) + prog = name; + else + prog++; + } + + +/* + * Log an error. + */ +void +err(char *fmt, ...) + { + va_list ap; + time_t now; + struct tm *tm; + FILE *fp; + + if ((fp = error_fp) == NULL) + { + fp = stderr; + if (prog != NULL) + fprintf(fp, "%s: ", prog); + } + else + { + time(&now); + tm = localtime(&now); + fprintf(fp, "%04d-%02d-%02d %02d:%02d ", tm->tm_year+1900, + tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min); + } + + va_start(ap, fmt); + vfprintf(fp, fmt, ap); + va_end(ap); + + fprintf(fp, "\n"); + fflush(fp); + } diff --git a/usr.sbin/ctm/ctm_rmail/error.h b/usr.sbin/ctm/ctm_rmail/error.h new file mode 100644 index 000000000000..b8bc4521e106 --- /dev/null +++ b/usr.sbin/ctm/ctm_rmail/error.h @@ -0,0 +1,3 @@ +extern void err_set_log(char *log_file); +extern void err_prog_name(char *name); +extern void err(char *fmt, ...); diff --git a/usr.sbin/ctm/ctm_rmail/options.h b/usr.sbin/ctm/ctm_rmail/options.h new file mode 100644 index 000000000000..829a9135a6c4 --- /dev/null +++ b/usr.sbin/ctm/ctm_rmail/options.h @@ -0,0 +1,139 @@ +/* + * Macros for processing command arguments. + * + * Conforms closely to the command option requirements of intro(1) in System V + * and intro(C) in Xenix. + * + * A command consists of: cmdname [ options ] [ cmdarguments ] + * + * Options consist of a leading dash '-' and a flag letter. An argument may + * follow optionally preceded by white space. + * Options without arguments may be grouped behind a single dash. + * A dash on its own is interpreted as the end of the options and is retained + * as a command argument. + * A double dash '--' is interpreted as the end of the options and is discarded. + * + * For example: + * zap -xz -f flame -q34 -- -x + * + * where zap.c contains the following in main(): + * + * OPTIONS("[-xz] [-q queue-id] [-f dump-file] user") + * FLAG('x', xecute) + * FLAG('z', zot) + * STRING('f', file) + * fp = fopen(file, "w"); + * NUMBER('q', queue) + * ENDOPTS + * + * Results in: + * xecute = 1 + * zot = 1 + * file = "flame" + * fp = fopen("flame", "w") + * queue = 34 + * argc = 2 + * argv[0] = "zap" + * argv[1] = "-x" + * + * Should the user enter unknown flags or leave out required arguments, + * the message: + * + * Usage: zap [-xz] [-q queue-id] [-f dump-file] user + * + * will be printed. This message can be printed by calling pusage(), or + * usage(). usage() will also cause program termination with exit code 1. + * + * Author: Stephen McKay, February 1991 + * + * Based on recollection of the original options.h produced at the University + * of Queensland by Ross Patterson (and possibly others). + */ + +static char *O_usage; +static char *O_name; +extern long atol(); + +void +pusage() + { + /* + * Avoid gratuitously loading stdio. + */ + write(2, "Usage: ", 7); + write(2, O_name, strlen(O_name)); + write(2, " ", 1); + write(2, O_usage, strlen(O_usage)); + write(2, "\n", 1); + } + +#define usage() (pusage(), exit(1)) + +#define OPTIONS(usage_msg) \ + { \ + char O_cont; \ + O_usage = (usage_msg); \ + O_name = argv[0]; \ + while (*++argv && **argv == '-') \ + { \ + if ((*argv)[1] == '\0') \ + break; \ + argc--; \ + if ((*argv)[1] == '-' && (*argv)[2] == '\0') \ + { \ + argv++; \ + break; \ + } \ + O_cont = 1; \ + while (O_cont) \ + switch (*++*argv) \ + { \ + case '-': \ + usage(); \ + case '\0': \ + O_cont = 0; + +#define FLAG(x,flag) \ + break; \ + case (x): \ + (flag) = 1; + +#define CHAR(x,ch) \ + break; \ + case (x): \ + O_cont = 0; \ + if (*++*argv == '\0' && (--argc, *++argv == 0)) \ + usage(); \ + (ch) = **argv; + +#define NUMBER(x,n) \ + break; \ + case (x): \ + O_cont = 0; \ + if (*++*argv == '\0' && (--argc, *++argv == 0)) \ + usage(); \ + (n) = atol(*argv); + +#define STRING(x,str) \ + break; \ + case (x): \ + O_cont = 0; \ + if (*++*argv == '\0' && (--argc, *++argv == 0)) \ + usage(); \ + (str) = *argv; + +#define SUFFIX(x,str) \ + break; \ + case (x): \ + (str) = ++*argv; \ + O_cont = 0; + +#define ENDOPTS \ + break; \ + default: \ + usage(); \ + } \ + } \ +O_end: \ + *--argv = O_name; \ + } diff --git a/usr.sbin/ctm/ctm_smail/Makefile b/usr.sbin/ctm/ctm_smail/Makefile new file mode 100644 index 000000000000..085899c1001e --- /dev/null +++ b/usr.sbin/ctm/ctm_smail/Makefile @@ -0,0 +1,7 @@ +PROG= ctm_smail +SRCS= ctm_smail.c error.c +NOMAN= 1 +CFLAGS+= -Wall -g -I${.CURDIR}/../ctm_rmail + +.include +.PATH: ${.CURDIR}/../ctm_rmail diff --git a/usr.sbin/ctm/ctm_smail/ctm_smail.c b/usr.sbin/ctm/ctm_smail/ctm_smail.c new file mode 100644 index 000000000000..b0379a21efa4 --- /dev/null +++ b/usr.sbin/ctm/ctm_smail/ctm_smail.c @@ -0,0 +1,324 @@ +/* + * Send a compressed CTM delta to a recipient mailing list by encoding it + * in safe ASCII characters, in mailer-friendly chunks, and passing it + * to sendmail. The encoding is almost the same as MIME BASE64, and is + * protected by a simple checksum. + * + * Author: Stephen McKay + * + * NOTICE: This is free software. I hope you get some use from this program. + * In return you should think about all the nice people who give away software. + * Maybe you should write some free software too. + */ + +#include +#include +#include +#include +#include +#include +#include "error.h" +#include "options.h" + +#define DEF_MAX_MSG 64000 /* Default maximum mail msg minus headers. */ + +#define LINE_LENGTH 76 /* Chars per encode line. Divisible by 4. */ + +void chop_and_send(char *delta, off_t ctm_size, long max_msg_size, + char *mail_alias); +unsigned encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size); +void write_header(FILE *sfp, char *mail_alias, char *delta, int pce, + int npieces); +void write_trailer(FILE *sfp, unsigned sum); +void apologise(char *delta, off_t ctm_size, long max_ctm_size, + char *mail_alias); +FILE *open_sendmail(void); +int close_sendmail(FILE *fp); + + +main(int argc, char **argv) + { + char *delta_file; + char *mail_alias; + long max_msg_size = DEF_MAX_MSG; + long max_ctm_size = 0; + char *log_file = NULL; + struct stat sb; + + err_prog_name(argv[0]); + + OPTIONS("[-l log] [-m maxmsgsize] [-c maxctmsize] ctm-delta mail-alias") + NUMBER('m', max_msg_size) + NUMBER('c', max_ctm_size) + STRING('l', log_file) + ENDOPTS + + if (argc != 3) + usage(); + + if (log_file != NULL) + err_set_log(log_file); + + delta_file = argv[1]; + mail_alias = argv[2]; + + if (stat(delta_file, &sb) < 0) + { + err("%s: %s", delta_file, strerror(errno)); + exit(1); + } + + if (max_ctm_size != 0 && sb.st_size > max_ctm_size) + apologise(delta_file, sb.st_size, max_ctm_size, mail_alias); + else + chop_and_send(delta_file, sb.st_size, max_msg_size, mail_alias); + + return 0; + } + + +/* + * Carve our CTM delta into pieces, encode them, and send them. + */ +void +chop_and_send(char *delta, off_t ctm_size, long max_msg_size, char *mail_alias) + { + int npieces; + long msg_size; + long exp_size; + int pce; + FILE *sfp; + FILE *dfp; + unsigned sum; + +#define howmany(x,y) (((x)+((y)-1))/(y)) + + /* + * Work out how many pieces we need, bearing in mind that each piece + * grows by 4/3 when encoded. We count the newlines too, but ignore + * all mail headers and piece headers. They are a "small" (almost + * constant) per message overhead that we make the user worry about. :-) + */ + exp_size = ctm_size * 4 / 3; + exp_size += howmany(exp_size, LINE_LENGTH); + npieces = howmany(exp_size, max_msg_size); + msg_size = howmany(ctm_size, npieces); + +#undef howmany + + if ((dfp = fopen(delta, "r")) == NULL) + { + err("cannot open '%s' for reading.", delta); + exit(1); + } + + for (pce = 1; pce <= npieces; pce++) + { + sfp = open_sendmail(); + if (sfp == NULL) + exit(1); + write_header(sfp, mail_alias, delta, pce, npieces); + sum = encode_body(sfp, dfp, msg_size); + write_trailer(sfp, sum); + if (!close_sendmail(sfp)) + exit(1); + err("%s %d/%d sent to %s", delta, pce, npieces, mail_alias); + } + + fclose(dfp); + } + + +/* + * MIME BASE64 encode table. + */ +static char to_b64[0x40] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/* + * This cheap plastic checksum effectively rotates our checksum-so-far + * left one, then adds the character. We only want 16 bits of it, and + * don't care what happens to the rest. It ain't much, but it's small. + */ +#define add_ck(sum,x) \ + ((sum) += ((x)&0xff) + (sum) + (((sum)&0x8000) ? 1 : 0)) + +/* + * Encode the body. Use an encoding almost the same as MIME BASE64. + * + * Characters are read from delta_fp and encoded characters are written + * to sm_fp. At most 'msg_size' characters should be read from delta_fp. + * + * The body consists of lines of up to LINE_LENGTH characters. Each group + * of 4 characters encodes 3 input characters. Each output character encodes + * 6 bits. Thus 64 different characters are needed in this representation. + */ +unsigned +encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size) + { + unsigned short cksum = 0xffff; + unsigned char *ip; + char *op; + int want, n, i; + unsigned char inbuf[LINE_LENGTH*3/4]; + char outbuf[LINE_LENGTH+1]; + + /* + * Round up to the nearest line boundary, for the tiniest of gains, + * and lots of neatness. :-) + */ + msg_size += (LINE_LENGTH*3/4) - 1; + msg_size -= msg_size % (LINE_LENGTH*3/4); + + while (msg_size > 0) + { + want = (msg_size < sizeof(inbuf)) ? msg_size : sizeof(inbuf); + if ((n = fread(inbuf, sizeof(char), want, delta_fp)) == 0) + break; + msg_size -= n; + + for (i = 0; i < n; i++) + add_ck(cksum, inbuf[i]); + + /* + * Produce a line of encoded data. Every line length will be a + * multiple of 4, except for, perhaps, the last line. + */ + ip = inbuf; + op = outbuf; + while (n >= 3) + { + *op++ = to_b64[ip[0] >> 2]; + *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4]; + *op++ = to_b64[(ip[1] << 2 & 0x3f) | ip[2] >> 6]; + *op++ = to_b64[ip[2] & 0x3f]; + ip += 3; + n -= 3; + } + if (n > 0) + { + *op++ = to_b64[ip[0] >> 2]; + *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4]; + if (n >= 2) + *op++ = to_b64[ip[1] << 2 & 0x3f]; + } + *op++ = '\n'; + fwrite(outbuf, sizeof(char), op - outbuf, sm_fp); + } + + if (ferror(delta_fp)) + { + err("error reading input file."); + exit(1); + } + + if (ferror(sm_fp)) + { + err("error writing to sendmail"); + exit(1); + } + + return cksum; + } + + +/* + * Write the mail header and data header. + */ +void +write_header(FILE *sfp, char *mail_alias, char *delta, int pce, int npieces) + { + char *sn; + + if ((sn = strrchr(delta, '/')) == NULL) + sn = delta; + else + sn++; + + fprintf(sfp, "From: %s-owner\n", mail_alias); + fprintf(sfp, "To: %s\n", mail_alias); + fprintf(sfp, "Subject: ctm-mail %s %d/%d\n\n", sn, pce, npieces); + + fprintf(sfp, "CTM_MAIL BEGIN %s %d %d\n", sn, pce, npieces); + } + + +/* + * Write the data trailer. + */ +void +write_trailer(FILE *sfp, unsigned sum) + { + fprintf(sfp, "CTM_MAIL END %ld\n", (long)sum); + } + + +/* + * We're terribly sorry, but the delta is too big to send. + */ +void +apologise(char *delta, off_t ctm_size, long max_ctm_size, char *mail_alias) + { + FILE *sfp; + char *sn; + + sfp = open_sendmail(); + if (sfp == NULL) + exit(1); + + if ((sn = strrchr(delta, '/')) == NULL) + sn = delta; + else + sn++; + + fprintf(sfp, "From: %s-owner\n", mail_alias); + fprintf(sfp, "To: %s\n", mail_alias); + fprintf(sfp, "Subject: ctm-notice %s\n\n", sn); + + fprintf(sfp, "%s is %ld bytes. The limit is %ld bytes.\n\n", sn, + (long)ctm_size, max_ctm_size); + fprintf(sfp, "You can retrieve this delta via ftpmail, or your good mate at the university.\n"); + + if (!close_sendmail(sfp)) + exit(1); + } + + +/* + * Start a pipe to sendmail. Sendmail will decode the destination + * from the message contents. + */ +FILE * +open_sendmail() + { + FILE *fp; + char buf[100]; + + sprintf(buf, "%s -t", _PATH_SENDMAIL); + if ((fp = popen(buf, "w")) == NULL) + err("cannot start sendmail"); + return fp; + } + + +/* + * Close a pipe to sendmail. Sendmail will then do its bit. + * Return 1 on success, 0 on failure. + */ +int +close_sendmail(FILE *fp) + { + int status; + + fflush(fp); + if (ferror(fp)) + { + err("error writing to sendmail"); + return 0; + } + + if ((status = pclose(fp)) != 0) + err("sendmail failed with status %d", status); + + return (status == 0); + }