ctm_smail.c revision 18120
1/*
2 * Send a compressed CTM delta to a recipient mailing list by encoding it
3 * in safe ASCII characters, in mailer-friendly chunks, and passing it
4 * to sendmail.  The encoding is almost the same as MIME BASE64, and is
5 * protected by a simple checksum.
6 *
7 * Author: Stephen McKay
8 *
9 * NOTICE: This is free software.  I hope you get some use from this program.
10 * In return you should think about all the nice people who give away software.
11 * Maybe you should write some free software too.
12 *
13 * $Id: ctm_smail.c,v 1.7 1996/09/07 18:48:44 peter Exp $
14 */
15
16#include <stdio.h>
17#include <stdlib.h>
18#include <string.h>
19#include <unistd.h>
20#include <fcntl.h>
21#include <sys/types.h>
22#include <sys/stat.h>
23#include <errno.h>
24#include <paths.h>
25#include "error.h"
26#include "options.h"
27
28#define DEF_MAX_MSG	64000	/* Default maximum mail msg minus headers. */
29
30#define LINE_LENGTH	76	/* Chars per encode line. Divisible by 4. */
31
32void chop_and_send(char *delta, off_t ctm_size, long max_msg_size,
33	char *mail_alias);
34void chop_and_queue(char *delta, off_t ctm_size, long max_msg_size,
35	char *queue_dir, char *mail_alias);
36unsigned encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size);
37void write_header(FILE *sfp, char *mail_alias, char *delta, int pce,
38	int npieces);
39void write_trailer(FILE *sfp, unsigned sum);
40void apologise(char *delta, off_t ctm_size, long max_ctm_size,
41	char *mail_alias);
42FILE *open_sendmail(void);
43int close_sendmail(FILE *fp);
44int lock_queuedir(char *queue_dir);
45void free_lock(int lockf, char *queue_dir);
46void add_to_queue(char *queue_dir, char *mail_alias, char *delta, int npieces, char **tempnames);
47
48int
49main(int argc, char **argv)
50    {
51    char *delta_file;
52    char *mail_alias;
53    long max_msg_size = DEF_MAX_MSG;
54    long max_ctm_size = 0;
55    char *log_file = NULL;
56    char *queue_dir = NULL;
57    struct stat sb;
58
59    err_prog_name(argv[0]);
60
61    OPTIONS("[-l log] [-m maxmsgsize] [-c maxctmsize] [-q queuedir] ctm-delta mail-alias")
62	NUMBER('m', max_msg_size)
63	NUMBER('c', max_ctm_size)
64	STRING('l', log_file)
65	STRING('q', queue_dir)
66    ENDOPTS
67
68    if (argc != 3)
69	usage();
70
71    if (log_file != NULL)
72	err_set_log(log_file);
73
74    delta_file = argv[1];
75    mail_alias = argv[2];
76
77    if (stat(delta_file, &sb) < 0)
78	{
79	err("%s: %s", delta_file, strerror(errno));
80	exit(1);
81	}
82
83    if (max_ctm_size != 0 && sb.st_size > max_ctm_size)
84	apologise(delta_file, sb.st_size, max_ctm_size, mail_alias);
85    else if (queue_dir == NULL)
86	chop_and_send(delta_file, sb.st_size, max_msg_size, mail_alias);
87    else
88	chop_and_queue(delta_file, sb.st_size, max_msg_size, queue_dir, mail_alias);
89
90    return 0;
91    }
92
93
94/*
95 * Carve our CTM delta into pieces, encode them, and send them.
96 */
97void
98chop_and_send(char *delta, off_t ctm_size, long max_msg_size, char *mail_alias)
99    {
100    int npieces;
101    long msg_size;
102    long exp_size;
103    int pce;
104    FILE *sfp;
105    FILE *dfp;
106    unsigned sum;
107    char *deltaname;
108
109#ifdef howmany
110#undef howmany
111#endif
112
113#define	howmany(x, y)	(((x) + ((y) - 1)) / (y))
114
115    /*
116     * Work out how many pieces we need, bearing in mind that each piece
117     * grows by 4/3 when encoded.  We count the newlines too, but ignore
118     * all mail headers and piece headers.  They are a "small" (almost
119     * constant) per message overhead that we make the user worry about. :-)
120     */
121    exp_size = ctm_size * 4 / 3;
122    exp_size += howmany(exp_size, LINE_LENGTH);
123    npieces = howmany(exp_size, max_msg_size);
124    msg_size = howmany(ctm_size, npieces);
125
126#undef howmany
127
128    if ((dfp = fopen(delta, "r")) == NULL)
129	{
130	err("cannot open '%s' for reading.", delta);
131	exit(1);
132	}
133
134    deltaname = strrchr(delta, '/');
135    if (deltaname)
136	deltaname++;	/* skip slash */
137    else
138	deltaname = delta;
139
140    for (pce = 1; pce <= npieces; pce++)
141	{
142	sfp = open_sendmail();
143	if (sfp == NULL)
144	    exit(1);
145	write_header(sfp, mail_alias, delta, pce, npieces);
146	sum = encode_body(sfp, dfp, msg_size);
147	write_trailer(sfp, sum);
148	if (!close_sendmail(sfp))
149	    exit(1);
150	err("%s %d/%d sent to %s", deltaname, pce, npieces, mail_alias);
151	}
152
153    fclose(dfp);
154    }
155
156/*
157 * Carve our CTM delta into pieces, encode them, and drop them in the
158 * queue dir.
159 *
160 * Basic algorythm:
161 *
162 * - for (each piece)
163 * -   gen. temp. file name (one which the de-queuer will ignore)
164 * -   record in array
165 * -   open temp. file
166 * -   encode delta (including headers) into the temp file
167 * -   close temp. file
168 * - end
169 * - lock queue directory
170 * - foreach (temp. file)
171 * -   rename to the proper filename
172 * - end
173 * - unlock queue directory
174 *
175 * This is probably overkill, but it means that incomplete deltas
176 * don't get mailed, and also reduces the window for lock races
177 * between ctm_smail and the de-queueing process.
178 */
179
180void
181chop_and_queue(char *delta, off_t ctm_size, long max_msg_size, char *queue_dir, char *mail_alias)
182{
183    int npieces, pce, len;
184    long msg_size, exp_size;
185    FILE *sfp, *dfp;
186    unsigned sum;
187    char **tempnames, *tempnam, *sn;
188
189#define	howmany(x, y)	(((x) + ((y) - 1)) / (y))
190
191    /*
192     * Work out how many pieces we need, bearing in mind that each piece
193     * grows by 4/3 when encoded.  We count the newlines too, but ignore
194     * all mail headers and piece headers.  They are a "small" (almost
195     * constant) per message overhead that we make the user worry about. :-)
196     */
197    exp_size = ctm_size * 4 / 3;
198    exp_size += howmany(exp_size, LINE_LENGTH);
199    npieces = howmany(exp_size, max_msg_size);
200    msg_size = howmany(ctm_size, npieces);
201
202#undef howmany
203
204    /*
205     * allocate space for the array of filenames. Try to be portable
206     * by not assuming anything to do with sizeof(char *)
207     */
208    tempnames = malloc(npieces * sizeof(char *));
209    if (tempnames == NULL)
210    {
211	err("malloc for tempnames failed");
212	exit(1);
213    }
214
215    len = strlen(queue_dir) + 16;
216    tempnam = malloc(len);
217    if (tempnam == NULL)
218    {
219	err("malloc for tempnames failed");
220	exit(1);
221    }
222
223    if ((dfp = fopen(delta, "r")) == NULL)
224    {
225	err("cannot open '%s' for reading.", delta);
226	exit(1);
227    }
228
229    if ((sn = strrchr(delta, '/')) == NULL)
230	sn = delta;
231    else
232	sn++;
233
234    for (pce = 1; pce <= npieces; pce++)
235    {
236	if (snprintf(tempnam, len, "%s/.%08d-%03d", queue_dir, getpid(), pce) >= len)
237	    err("Whoops! tempnam isn't long enough");
238
239	tempnames[pce - 1] = strdup(tempnam);
240	if (tempnames[pce - 1] == NULL)
241	{
242	    err("strdup failed for temp. filename");
243	    exit(1);
244	}
245
246	sfp = fopen(tempnam, "w");
247	if (sfp == NULL)
248	    exit(1);
249
250	write_header(sfp, mail_alias, delta, pce, npieces);
251	sum = encode_body(sfp, dfp, msg_size);
252	write_trailer(sfp, sum);
253
254	if (fclose(sfp) != 0)
255	    exit(1);
256
257	err("%s %d/%d created succesfully", sn, pce, npieces);
258    }
259
260    add_to_queue(queue_dir, mail_alias, delta, npieces, tempnames);
261
262    fclose(dfp);
263
264}
265
266
267/*
268 * MIME BASE64 encode table.
269 */
270static char to_b64[0x40] =
271    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
272
273/*
274 * This cheap plastic checksum effectively rotates our checksum-so-far
275 * left one, then adds the character.  We only want 16 bits of it, and
276 * don't care what happens to the rest.  It ain't much, but it's small.
277 */
278#define add_ck(sum,x)	\
279    ((sum) += ((x)&0xff) + (sum) + (((sum)&0x8000) ? 1 : 0))
280
281/*
282 * Encode the body.  Use an encoding almost the same as MIME BASE64.
283 *
284 * Characters are read from delta_fp and encoded characters are written
285 * to sm_fp.  At most 'msg_size' characters should be read from delta_fp.
286 *
287 * The body consists of lines of up to LINE_LENGTH characters.  Each group
288 * of 4 characters encodes 3 input characters.  Each output character encodes
289 * 6 bits.  Thus 64 different characters are needed in this representation.
290 */
291unsigned
292encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size)
293    {
294    unsigned short cksum = 0xffff;
295    unsigned char *ip;
296    char *op;
297    int want, n, i;
298    unsigned char inbuf[LINE_LENGTH*3/4];
299    char outbuf[LINE_LENGTH+1];
300
301    /*
302     * Round up to the nearest line boundary, for the tiniest of gains,
303     * and lots of neatness. :-)
304     */
305    msg_size += (LINE_LENGTH*3/4) - 1;
306    msg_size -= msg_size % (LINE_LENGTH*3/4);
307
308    while (msg_size > 0)
309	{
310	want = (msg_size < sizeof(inbuf)) ? msg_size : sizeof(inbuf);
311	if ((n = fread(inbuf, sizeof(char), want, delta_fp)) == 0)
312	    break;
313	msg_size -= n;
314
315	for (i = 0; i < n; i++)
316	    add_ck(cksum, inbuf[i]);
317
318	/*
319	 * Produce a line of encoded data.  Every line length will be a
320	 * multiple of 4, except for, perhaps, the last line.
321	 */
322	ip = inbuf;
323	op = outbuf;
324	while (n >= 3)
325	    {
326	    *op++ = to_b64[ip[0] >> 2];
327	    *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4];
328	    *op++ = to_b64[(ip[1] << 2 & 0x3f) | ip[2] >> 6];
329	    *op++ = to_b64[ip[2] & 0x3f];
330	    ip += 3;
331	    n -= 3;
332	    }
333	if (n > 0)
334	    {
335	    *op++ = to_b64[ip[0] >> 2];
336	    *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4];
337	    if (n >= 2)
338		*op++ = to_b64[ip[1] << 2 & 0x3f];
339	    }
340	*op++ = '\n';
341	fwrite(outbuf, sizeof(char), op - outbuf, sm_fp);
342	}
343
344    if (ferror(delta_fp))
345	{
346	err("error reading input file.");
347	exit(1);
348	}
349
350    if (ferror(sm_fp))
351	{
352	err("error writing encoded file");
353	exit(1);
354	}
355
356    return cksum;
357    }
358
359
360/*
361 * Write the mail header and data header.
362 */
363void
364write_header(FILE *sfp, char *mail_alias, char *delta, int pce, int npieces)
365    {
366    char *sn;
367
368    if ((sn = strrchr(delta, '/')) == NULL)
369	sn = delta;
370    else
371	sn++;
372
373    fprintf(sfp, "From: owner-%s\n", mail_alias);
374    fprintf(sfp, "To: %s\n", mail_alias);
375    fprintf(sfp, "Subject: ctm-mail %s %d/%d\n\n", sn, pce, npieces);
376
377    fprintf(sfp, "CTM_MAIL BEGIN %s %d %d\n", sn, pce, npieces);
378    }
379
380
381/*
382 * Write the data trailer.
383 */
384void
385write_trailer(FILE *sfp, unsigned sum)
386    {
387    fprintf(sfp, "CTM_MAIL END %ld\n", (long)sum);
388    }
389
390
391/*
392 * We're terribly sorry, but the delta is too big to send.
393 */
394void
395apologise(char *delta, off_t ctm_size, long max_ctm_size, char *mail_alias)
396    {
397    FILE *sfp;
398    char *sn;
399
400    sfp = open_sendmail();
401    if (sfp == NULL)
402	exit(1);
403
404    if ((sn = strrchr(delta, '/')) == NULL)
405	sn = delta;
406    else
407	sn++;
408
409    fprintf(sfp, "From: %s-owner\n", mail_alias);
410    fprintf(sfp, "To: %s\n", mail_alias);
411    fprintf(sfp, "Subject: ctm-notice %s\n\n", sn);
412
413    fprintf(sfp, "%s is %ld bytes.  The limit is %ld bytes.\n\n", sn,
414	(long)ctm_size, max_ctm_size);
415    fprintf(sfp, "You can retrieve this delta via ftpmail, or your good mate at the university.\n");
416
417    if (!close_sendmail(sfp))
418	exit(1);
419    }
420
421
422/*
423 * Start a pipe to sendmail.  Sendmail will decode the destination
424 * from the message contents.
425 */
426FILE *
427open_sendmail()
428    {
429    FILE *fp;
430    char buf[100];
431
432    sprintf(buf, "%s -odq -t", _PATH_SENDMAIL);
433    if ((fp = popen(buf, "w")) == NULL)
434	err("cannot start sendmail");
435    return fp;
436    }
437
438
439/*
440 * Close a pipe to sendmail.  Sendmail will then do its bit.
441 * Return 1 on success, 0 on failure.
442 */
443int
444close_sendmail(FILE *fp)
445    {
446    int status;
447
448    fflush(fp);
449    if (ferror(fp))
450	{
451	err("error writing to sendmail");
452	return 0;
453	}
454
455    if ((status = pclose(fp)) != 0)
456	err("sendmail failed with status %d", status);
457
458    return (status == 0);
459    }
460
461/*
462 * Lock the queuedir so we're the only guy messing about in there.
463 */
464int
465lock_queuedir(char *queue_dir)
466{
467    int fp, len;
468    char *buffer;
469    struct stat sb;
470
471    len = strlen(queue_dir) + 8;
472
473    buffer = malloc(len);
474    if (buffer == NULL)
475    {
476	err("malloc failed in lock_queuedir");
477	exit(1);
478    }
479
480    if (snprintf(buffer, len, "%s/.lock", queue_dir) >= len)
481	err("Whoops. lock buffer too small in lock_queuedir");
482
483    /*
484     * We do our own lockfile scanning to avoid unlink races. 60
485     * seconds should be enough to ensure that we won't get more races
486     * happening between the stat and the open/flock.
487     */
488
489    while (stat(buffer, &sb) == 0)
490	sleep(60);
491
492    if ((fp = open(buffer, O_WRONLY | O_CREAT | O_EXLOCK, 0600)) < 0)
493    {
494	err("can't open `%s' in lock_queuedir", buffer);
495	exit(1);
496    }
497
498    snprintf(buffer, len, "%8ld", getpid());
499    write(fp, buffer, 8);
500
501    free(buffer);
502
503    return(fp);
504}
505
506/*
507 * Lock the queuedir so we're the only guy messing about in there.
508 */
509void
510free_lock(int lockf, char *queue_dir)
511{
512    int len;
513    char *path;
514
515    /*
516     * Most important: free the lock before we do anything else!
517     */
518
519    close(lockf);
520
521    len = strlen(queue_dir) + 7;
522
523    path = malloc(len);
524    if (path == NULL)
525    {
526	err("malloc failed in free_lock");
527	exit(1);
528    }
529
530    if (snprintf(path, len, "%s/.lock", queue_dir) >= len)
531	err("lock path buffer too small in free_lock");
532
533    if (unlink(path) != 0)
534    {
535	err("can't unlink lockfile `%s'", path);
536	exit(1);
537    }
538
539    free(path);
540}
541
542/* move everything into the queue directory. */
543
544void
545add_to_queue(char *queue_dir, char *mail_alias, char *delta, int npieces, char **tempnames)
546{
547    char *queuefile, *sn;
548    int pce, len, lockf;
549
550    if ((sn = strrchr(delta, '/')) == NULL)
551	sn = delta;
552    else
553	sn++;
554
555    /* try to malloc all we need BEFORE entering the lock loop */
556
557    len = strlen(queue_dir) + strlen(sn) + 7;
558    queuefile = malloc(len);
559    if (queuefile == NULL)
560    {
561	err("can't malloc for queuefile");
562	exit(1);
563    }
564
565    /*
566     * We should be the only process mucking around in the queue
567     * directory while we add the new queue files ... it could be
568     * awkward if the de-queue process starts it's job while we're
569     * adding files ...
570     */
571
572    lockf = lock_queuedir(queue_dir);
573    for (pce = 0; pce < npieces; pce++)
574    {
575	struct stat sb;
576
577	if (snprintf(queuefile, len, "%s/%s+%03d", queue_dir, sn, pce + 1) >= len)
578	    err("whoops, queuefile buffer is too small");
579
580	if (stat(queuefile, &sb) == 0)
581	{
582	    err("WOAH! Queue file `%s' already exists! Bailing out.", queuefile);
583	    free_lock(lockf, queue_dir);
584	    exit(1);
585	}
586
587	rename(tempnames[pce], queuefile);
588    }
589
590    free_lock(lockf, queue_dir);
591
592    free(queuefile);
593}
594