16081Sphk/*
28857Srgrimes * Send a compressed CTM delta to a recipient mailing list by encoding it
319983Smckay * in safe ASCII characters, in mailer-friendly chunks, and passing them
419983Smckay * to sendmail.  Optionally, the chunks can be queued to be sent later by
519983Smckay * ctm_dequeue in controlled bursts.  The encoding is almost the same as
619983Smckay * MIME BASE64, and is protected by a simple checksum.
76081Sphk *
86081Sphk * Author: Stephen McKay
96081Sphk *
108857Srgrimes * NOTICE: This is free software.  I hope you get some use from this program.
118857Srgrimes * In return you should think about all the nice people who give away software.
128857Srgrimes * Maybe you should write some free software too.
1316880Sgpalmer *
1450479Speter * $FreeBSD$
156081Sphk */
166081Sphk
176081Sphk#include <stdio.h>
1816880Sgpalmer#include <stdlib.h>
196081Sphk#include <string.h>
206290Sphk#include <unistd.h>
2116880Sgpalmer#include <fcntl.h>
226081Sphk#include <sys/types.h>
236081Sphk#include <sys/stat.h>
246081Sphk#include <errno.h>
256081Sphk#include <paths.h>
2619983Smckay#include <limits.h>
276081Sphk#include "error.h"
286081Sphk#include "options.h"
296081Sphk
306081Sphk#define DEF_MAX_MSG	64000	/* Default maximum mail msg minus headers. */
316081Sphk
32202918Smckay#define LINE_LENGTH	72	/* Chars per encoded line. Divisible by 4. */
336081Sphk
3419983Smckayint chop_and_send_or_queue(FILE *dfp, char *delta, off_t ctm_size,
3519983Smckay	long max_msg_size, char *mail_alias, char *queue_dir);
3619983Smckayint chop_and_send(FILE *dfp, char *delta, long msg_size, int npieces,
376081Sphk	char *mail_alias);
3819983Smckayint chop_and_queue(FILE *dfp, char *delta, long msg_size, int npieces,
3919983Smckay	char *mail_alias, char *queue_dir);
4019983Smckayvoid clean_up_queue(char *queue_dir);
4119983Smckayint encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size, unsigned *sum);
426081Sphkvoid write_header(FILE *sfp, char *mail_alias, char *delta, int pce,
436081Sphk	int npieces);
446081Sphkvoid write_trailer(FILE *sfp, unsigned sum);
4519983Smckayint apologise(char *delta, off_t ctm_size, long max_ctm_size,
46117503Skris	char *mail_alias, char *queue_dir);
476081SphkFILE *open_sendmail(void);
486081Sphkint close_sendmail(FILE *fp);
496081Sphk
506290Sphkint
516081Sphkmain(int argc, char **argv)
526081Sphk    {
5319983Smckay    int status = 0;
546081Sphk    char *delta_file;
556081Sphk    char *mail_alias;
566081Sphk    long max_msg_size = DEF_MAX_MSG;
576081Sphk    long max_ctm_size = 0;
586081Sphk    char *log_file = NULL;
5916880Sgpalmer    char *queue_dir = NULL;
6019983Smckay    char *delta;
6119983Smckay    FILE *dfp;
626081Sphk    struct stat sb;
636081Sphk
646081Sphk    err_prog_name(argv[0]);
656081Sphk
6616880Sgpalmer    OPTIONS("[-l log] [-m maxmsgsize] [-c maxctmsize] [-q queuedir] ctm-delta mail-alias")
676081Sphk	NUMBER('m', max_msg_size)
686081Sphk	NUMBER('c', max_ctm_size)
696081Sphk	STRING('l', log_file)
7016880Sgpalmer	STRING('q', queue_dir)
716081Sphk    ENDOPTS
726081Sphk
736081Sphk    if (argc != 3)
746081Sphk	usage();
756081Sphk
766081Sphk    if (log_file != NULL)
776081Sphk	err_set_log(log_file);
786081Sphk
796081Sphk    delta_file = argv[1];
806081Sphk    mail_alias = argv[2];
816081Sphk
8219983Smckay    if ((delta = strrchr(delta_file, '/')) == NULL)
8319983Smckay	delta = delta_file;
8419983Smckay    else
8519983Smckay	delta++;
8619983Smckay
8719983Smckay    if ((dfp = fopen(delta_file, "r")) == NULL || fstat(fileno(dfp), &sb) < 0)
886081Sphk	{
8919983Smckay	err("*%s", delta_file);
906081Sphk	exit(1);
916081Sphk	}
928857Srgrimes
936081Sphk    if (max_ctm_size != 0 && sb.st_size > max_ctm_size)
94117503Skris	status = apologise(delta, sb.st_size, max_ctm_size, mail_alias,
95117503Skris		queue_dir);
966081Sphk    else
9719983Smckay	status = chop_and_send_or_queue(dfp, delta, sb.st_size, max_msg_size,
9819983Smckay		mail_alias, queue_dir);
996081Sphk
10019983Smckay    fclose(dfp);
10119983Smckay
10219983Smckay    return status;
1036081Sphk    }
1046081Sphk
1056081Sphk
1066081Sphk/*
10719983Smckay * Carve our CTM delta into pieces, encode them, and send or queue them.
10819983Smckay * Returns 0 on success, and 1 on failure.
1096081Sphk */
11019983Smckayint
11119983Smckaychop_and_send_or_queue(FILE *dfp, char *delta, off_t ctm_size,
11219983Smckay	long max_msg_size, char *mail_alias, char *queue_dir)
1136081Sphk    {
1146081Sphk    int npieces;
1156081Sphk    long msg_size;
1166081Sphk    long exp_size;
11719983Smckay    int status;
1186081Sphk
11916880Sgpalmer#undef howmany
12019983Smckay#define	howmany(x,y)	(((x)+((y)-1)) / (y))
12116880Sgpalmer
1226081Sphk    /*
1236081Sphk     * Work out how many pieces we need, bearing in mind that each piece
1246081Sphk     * grows by 4/3 when encoded.  We count the newlines too, but ignore
1256081Sphk     * all mail headers and piece headers.  They are a "small" (almost
1266081Sphk     * constant) per message overhead that we make the user worry about. :-)
1276081Sphk     */
1286081Sphk    exp_size = ctm_size * 4 / 3;
1296081Sphk    exp_size += howmany(exp_size, LINE_LENGTH);
1306081Sphk    npieces = howmany(exp_size, max_msg_size);
1316081Sphk    msg_size = howmany(ctm_size, npieces);
1326081Sphk
1336081Sphk#undef howmany
1346081Sphk
13519983Smckay    if (queue_dir == NULL)
13619983Smckay	status = chop_and_send(dfp, delta, msg_size, npieces, mail_alias);
13719983Smckay    else
1386081Sphk	{
13919983Smckay	status = chop_and_queue(dfp, delta, msg_size, npieces, mail_alias,
14019983Smckay		queue_dir);
14119983Smckay	if (status)
14219983Smckay	    clean_up_queue(queue_dir);
1436081Sphk	}
1446081Sphk
14519983Smckay    return status;
14619983Smckay    }
14718120Speter
14819983Smckay
14919983Smckay/*
15019983Smckay * Carve our CTM delta into pieces, encode them, and send them.
15119983Smckay * Returns 0 on success, and 1 on failure.
15219983Smckay */
15319983Smckayint
15419983Smckaychop_and_send(FILE *dfp, char *delta, long msg_size, int npieces,
15519983Smckay	char *mail_alias)
15619983Smckay    {
15719983Smckay    int pce;
15819983Smckay    FILE *sfp;
15919983Smckay    unsigned sum;
16019983Smckay
16119983Smckay    /*
16219983Smckay     * Send each chunk directly to sendmail as it is generated.
16319983Smckay     * No temporary files necessary.  If things turn ugly, we just
16419983Smckay     * have to live with the fact the we have sent only part of
16519983Smckay     * the delta.
16619983Smckay     */
1676081Sphk    for (pce = 1; pce <= npieces; pce++)
1686081Sphk	{
16919983Smckay	int read_error;
17019983Smckay
17119983Smckay	if ((sfp = open_sendmail()) == NULL)
17219983Smckay	    return 1;
17319983Smckay
1746081Sphk	write_header(sfp, mail_alias, delta, pce, npieces);
17519983Smckay	read_error = encode_body(sfp, dfp, msg_size, &sum);
17619983Smckay	if (!read_error)
17719983Smckay	    write_trailer(sfp, sum);
17819983Smckay
17919983Smckay	if (!close_sendmail(sfp) || read_error)
18019983Smckay	    return 1;
18119983Smckay
18219983Smckay	err("%s %d/%d sent to %s", delta, pce, npieces, mail_alias);
1836081Sphk	}
1846081Sphk
18519983Smckay    return 0;
1866081Sphk    }
1876081Sphk
18819983Smckay
18919983Smckay/*
19019983Smckay * Construct the tmp queue file name of a delta piece.
19116880Sgpalmer */
19219983Smckay#define mk_tmp_name(fn,qd,p) \
19319983Smckay    sprintf((fn), "%s/.%08ld.%03d", (qd), (long)getpid(), (p))
1946081Sphk
19519983Smckay/*
19619983Smckay * Construct the final queue file name of a delta piece.
19719983Smckay */
19819983Smckay#define mk_queue_name(fn,qd,d,p,n) \
19919983Smckay    sprintf((fn), "%s/%s+%03d-%03d", (qd), (d), (p), (n))
20019983Smckay
20119983Smckay/*
20219983Smckay * Carve our CTM delta into pieces, encode them, and queue them.
20319983Smckay * Returns 0 on success, and 1 on failure.
20419983Smckay */
20519983Smckayint
20619983Smckaychop_and_queue(FILE *dfp, char *delta, long msg_size, int npieces,
20719983Smckay	char *mail_alias, char *queue_dir)
20819983Smckay    {
20919983Smckay    int pce;
21019983Smckay    FILE *qfp;
21116880Sgpalmer    unsigned sum;
21219983Smckay    char tname[PATH_MAX];
21319983Smckay    char qname[PATH_MAX];
21416880Sgpalmer
21516880Sgpalmer    /*
21619983Smckay     * Store each piece in the queue directory, but under temporary names,
21719983Smckay     * so that they can be deleted without unpleasant consequences if
21819983Smckay     * anything goes wrong.  We could easily fill up a disk, for example.
21916880Sgpalmer     */
22019983Smckay    for (pce = 1; pce <= npieces; pce++)
22119983Smckay	{
22219983Smckay	int write_error;
22316880Sgpalmer
22419983Smckay	mk_tmp_name(tname, queue_dir, pce);
22519983Smckay	if ((qfp = fopen(tname, "w")) == NULL)
22619983Smckay	    {
22719983Smckay	    err("cannot open '%s' for writing", tname);
22819983Smckay	    return 1;
22919983Smckay	    }
23016880Sgpalmer
23119983Smckay	write_header(qfp, mail_alias, delta, pce, npieces);
23219983Smckay	if (encode_body(qfp, dfp, msg_size, &sum))
23319983Smckay	    return 1;
23419983Smckay	write_trailer(qfp, sum);
23519983Smckay
23619983Smckay	fflush(qfp);
23719983Smckay	write_error = ferror(qfp);
23819983Smckay	fclose(qfp);
23919983Smckay	if (write_error)
24019983Smckay	    {
24119983Smckay	    err("error writing '%s'", tname);
24219983Smckay	    return 1;
24319983Smckay	    }
24419983Smckay
24519983Smckay	/*
24619983Smckay	 * Give the warm success message now, instead of all in a rush
24719983Smckay	 * during the rename phase.
24819983Smckay	 */
24919983Smckay	err("%s %d/%d queued for %s", delta, pce, npieces, mail_alias);
25019983Smckay	}
25119983Smckay
25216880Sgpalmer    /*
25319983Smckay     * Rename the pieces into place.  If an error occurs now, we are
25419983Smckay     * stuffed, but there is no neat way to back out.  rename() should
25519983Smckay     * only fail now under extreme circumstances.
25616880Sgpalmer     */
25719983Smckay    for (pce = 1; pce <= npieces; pce++)
25819983Smckay	{
25919983Smckay	mk_tmp_name(tname, queue_dir, pce);
26019983Smckay	mk_queue_name(qname, queue_dir, delta, pce, npieces);
26119983Smckay	if (rename(tname, qname) < 0)
26219983Smckay	    {
26319983Smckay	    err("*rename: '%s' to '%s'", tname, qname);
26419983Smckay	    unlink(tname);
26519983Smckay	    }
26619983Smckay	}
26719983Smckay
26819983Smckay    return 0;
26916880Sgpalmer    }
27016880Sgpalmer
27116880Sgpalmer
27219983Smckay/*
27319983Smckay * There may be temporary files cluttering up the queue directory.
27419983Smckay */
27519983Smckayvoid
27619983Smckayclean_up_queue(char *queue_dir)
27716880Sgpalmer    {
27819983Smckay    int pce;
27919983Smckay    char tname[PATH_MAX];
28016880Sgpalmer
28119983Smckay    err("discarding queued delta pieces");
28219983Smckay    for (pce = 1; ; pce++)
28316880Sgpalmer	{
28419983Smckay	mk_tmp_name(tname, queue_dir, pce);
28519983Smckay	if (unlink(tname) < 0)
28619983Smckay	    break;
28716880Sgpalmer	}
28816880Sgpalmer    }
28916880Sgpalmer
29016880Sgpalmer
2916081Sphk/*
2926081Sphk * MIME BASE64 encode table.
2936081Sphk */
2946081Sphkstatic char to_b64[0x40] =
2956081Sphk    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2966081Sphk
2976081Sphk/*
2986081Sphk * This cheap plastic checksum effectively rotates our checksum-so-far
2996081Sphk * left one, then adds the character.  We only want 16 bits of it, and
3006081Sphk * don't care what happens to the rest.  It ain't much, but it's small.
3016081Sphk */
3026081Sphk#define add_ck(sum,x)	\
3036081Sphk    ((sum) += ((x)&0xff) + (sum) + (((sum)&0x8000) ? 1 : 0))
3046081Sphk
3056081Sphk/*
3066081Sphk * Encode the body.  Use an encoding almost the same as MIME BASE64.
3076081Sphk *
3086081Sphk * Characters are read from delta_fp and encoded characters are written
3096081Sphk * to sm_fp.  At most 'msg_size' characters should be read from delta_fp.
3106081Sphk *
3116081Sphk * The body consists of lines of up to LINE_LENGTH characters.  Each group
3126081Sphk * of 4 characters encodes 3 input characters.  Each output character encodes
3136081Sphk * 6 bits.  Thus 64 different characters are needed in this representation.
3146081Sphk */
31519983Smckayint
31619983Smckayencode_body(FILE *sm_fp, FILE *delta_fp, long msg_size, unsigned *sum)
3176081Sphk    {
3186081Sphk    unsigned short cksum = 0xffff;
3196081Sphk    unsigned char *ip;
3206081Sphk    char *op;
3216081Sphk    int want, n, i;
3226081Sphk    unsigned char inbuf[LINE_LENGTH*3/4];
3236081Sphk    char outbuf[LINE_LENGTH+1];
3246081Sphk
3256081Sphk    /*
3266081Sphk     * Round up to the nearest line boundary, for the tiniest of gains,
3276081Sphk     * and lots of neatness. :-)
3286081Sphk     */
3296081Sphk    msg_size += (LINE_LENGTH*3/4) - 1;
3306081Sphk    msg_size -= msg_size % (LINE_LENGTH*3/4);
3316081Sphk
3326081Sphk    while (msg_size > 0)
3336081Sphk	{
3346081Sphk	want = (msg_size < sizeof(inbuf)) ? msg_size : sizeof(inbuf);
3356081Sphk	if ((n = fread(inbuf, sizeof(char), want, delta_fp)) == 0)
3366081Sphk	    break;
3376081Sphk	msg_size -= n;
3386081Sphk
3396081Sphk	for (i = 0; i < n; i++)
3406081Sphk	    add_ck(cksum, inbuf[i]);
3416081Sphk
3426081Sphk	/*
3436081Sphk	 * Produce a line of encoded data.  Every line length will be a
3446081Sphk	 * multiple of 4, except for, perhaps, the last line.
3456081Sphk	 */
3466081Sphk	ip = inbuf;
3476081Sphk	op = outbuf;
3486081Sphk	while (n >= 3)
3496081Sphk	    {
3506081Sphk	    *op++ = to_b64[ip[0] >> 2];
3516081Sphk	    *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4];
3526081Sphk	    *op++ = to_b64[(ip[1] << 2 & 0x3f) | ip[2] >> 6];
3536081Sphk	    *op++ = to_b64[ip[2] & 0x3f];
3546081Sphk	    ip += 3;
3556081Sphk	    n -= 3;
3566081Sphk	    }
3576081Sphk	if (n > 0)
3586081Sphk	    {
3596081Sphk	    *op++ = to_b64[ip[0] >> 2];
3606081Sphk	    *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4];
3616081Sphk	    if (n >= 2)
3626081Sphk		*op++ = to_b64[ip[1] << 2 & 0x3f];
3636081Sphk	    }
3646081Sphk	*op++ = '\n';
3656081Sphk	fwrite(outbuf, sizeof(char), op - outbuf, sm_fp);
3666081Sphk	}
3676081Sphk
3686081Sphk    if (ferror(delta_fp))
3696081Sphk	{
3706081Sphk	err("error reading input file.");
37119983Smckay	return 1;
3726081Sphk	}
3736081Sphk
37419983Smckay    *sum = cksum;
3756081Sphk
37619983Smckay    return 0;
3776081Sphk    }
3786081Sphk
3796081Sphk
3806081Sphk/*
3816081Sphk * Write the mail header and data header.
3826081Sphk */
3836081Sphkvoid
3846081Sphkwrite_header(FILE *sfp, char *mail_alias, char *delta, int pce, int npieces)
3856081Sphk    {
3866457Sphk    fprintf(sfp, "From: owner-%s\n", mail_alias);
3876081Sphk    fprintf(sfp, "To: %s\n", mail_alias);
38819983Smckay    fprintf(sfp, "Subject: ctm-mail %s %d/%d\n\n", delta, pce, npieces);
3896081Sphk
39019983Smckay    fprintf(sfp, "CTM_MAIL BEGIN %s %d %d\n", delta, pce, npieces);
3916081Sphk    }
3926081Sphk
3936081Sphk
3946081Sphk/*
3956081Sphk * Write the data trailer.
3966081Sphk */
3976081Sphkvoid
3986081Sphkwrite_trailer(FILE *sfp, unsigned sum)
3996081Sphk    {
4006081Sphk    fprintf(sfp, "CTM_MAIL END %ld\n", (long)sum);
4016081Sphk    }
4026081Sphk
4036081Sphk
4046081Sphk/*
4056081Sphk * We're terribly sorry, but the delta is too big to send.
40619983Smckay * Returns 0 on success, 1 on failure.
4076081Sphk */
40819983Smckayint
409117503Skrisapologise(char *delta, off_t ctm_size, long max_ctm_size, char *mail_alias,
410117503Skris	char *queue_dir)
4116081Sphk    {
4126081Sphk    FILE *sfp;
413117503Skris    char qname[PATH_MAX];
4146081Sphk
415117503Skris    if (queue_dir == NULL)
416117503Skris	{
417117503Skris	sfp = open_sendmail();
418117503Skris	if (sfp == NULL)
419117503Skris	    return 1;
420117503Skris	}
421117503Skris    else
422117503Skris	{
423117503Skris	mk_queue_name(qname, queue_dir, delta, 1, 1);
424117503Skris	sfp = fopen(qname, "w");
425117503Skris	if (sfp == NULL)
426117503Skris	    {
427117503Skris	    err("cannot open '%s' for writing", qname);
428117503Skris	    return 1;
429117503Skris	    }
430117503Skris	}
4316081Sphk
432117503Skris
43319983Smckay    fprintf(sfp, "From: owner-%s\n", mail_alias);
4346081Sphk    fprintf(sfp, "To: %s\n", mail_alias);
43519983Smckay    fprintf(sfp, "Subject: ctm-notice %s\n\n", delta);
4366081Sphk
43719983Smckay    fprintf(sfp, "%s is %ld bytes.  The limit is %ld bytes.\n\n", delta,
4386081Sphk	(long)ctm_size, max_ctm_size);
439117503Skris    fprintf(sfp, "You can retrieve this delta via ftp.\n");
4406081Sphk
441117503Skris    if (queue_dir == NULL)
442117503Skris	{
443117503Skris	if (!close_sendmail(sfp))
444117503Skris	    return 1;
445117503Skris	}
446117503Skris    else
447117503Skris	{
448117503Skris	if (fclose(sfp)!=0)
449117503Skris	    {
450117503Skris	    err("error writing '%s'", qname);
451117503Skris	    unlink(qname);
452117503Skris	    return 1;
453117503Skris            }
454117503Skris	}
45519983Smckay
45619983Smckay    return 0;
4576081Sphk    }
4586081Sphk
4596081Sphk
4606081Sphk/*
4616081Sphk * Start a pipe to sendmail.  Sendmail will decode the destination
4626081Sphk * from the message contents.
4636081Sphk */
4646081SphkFILE *
4656081Sphkopen_sendmail()
4666081Sphk    {
4676081Sphk    FILE *fp;
4686081Sphk    char buf[100];
4696081Sphk
47018110Speter    sprintf(buf, "%s -odq -t", _PATH_SENDMAIL);
4716081Sphk    if ((fp = popen(buf, "w")) == NULL)
4726081Sphk	err("cannot start sendmail");
4736081Sphk    return fp;
4746081Sphk    }
4756081Sphk
4766081Sphk
4776081Sphk/*
4786081Sphk * Close a pipe to sendmail.  Sendmail will then do its bit.
4796081Sphk * Return 1 on success, 0 on failure.
4806081Sphk */
4816081Sphkint
4826081Sphkclose_sendmail(FILE *fp)
4836081Sphk    {
4846081Sphk    int status;
4856081Sphk
4866081Sphk    fflush(fp);
4876081Sphk    if (ferror(fp))
4886081Sphk	{
4896081Sphk	err("error writing to sendmail");
4906081Sphk	return 0;
4916081Sphk	}
4926081Sphk
4936081Sphk    if ((status = pclose(fp)) != 0)
4946081Sphk	err("sendmail failed with status %d", status);
4956081Sphk
4966081Sphk    return (status == 0);
4976081Sphk    }
498