Initial import from FreeBSD RELENG_4:
[dragonfly.git] / usr.sbin / ctm / ctm_smail / ctm_smail.c
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 them
4  * to sendmail.  Optionally, the chunks can be queued to be sent later by
5  * ctm_dequeue in controlled bursts.  The encoding is almost the same as
6  * MIME BASE64, and is protected by a simple checksum.
7  *
8  * Author: Stephen McKay
9  *
10  * NOTICE: This is free software.  I hope you get some use from this program.
11  * In return you should think about all the nice people who give away software.
12  * Maybe you should write some free software too.
13  *
14  * $FreeBSD: src/usr.sbin/ctm/ctm_smail/ctm_smail.c,v 1.12 1999/08/28 01:16:02 peter Exp $
15  */
16
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <string.h>
20 #include <unistd.h>
21 #include <fcntl.h>
22 #include <sys/types.h>
23 #include <sys/stat.h>
24 #include <errno.h>
25 #include <paths.h>
26 #include <limits.h>
27 #include "error.h"
28 #include "options.h"
29
30 #define DEF_MAX_MSG     64000   /* Default maximum mail msg minus headers. */
31
32 #define LINE_LENGTH     76      /* Chars per encoded line. Divisible by 4. */
33
34 int chop_and_send_or_queue(FILE *dfp, char *delta, off_t ctm_size,
35         long max_msg_size, char *mail_alias, char *queue_dir);
36 int chop_and_send(FILE *dfp, char *delta, long msg_size, int npieces,
37         char *mail_alias);
38 int chop_and_queue(FILE *dfp, char *delta, long msg_size, int npieces,
39         char *mail_alias, char *queue_dir);
40 void clean_up_queue(char *queue_dir);
41 int encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size, unsigned *sum);
42 void write_header(FILE *sfp, char *mail_alias, char *delta, int pce,
43         int npieces);
44 void write_trailer(FILE *sfp, unsigned sum);
45 int apologise(char *delta, off_t ctm_size, long max_ctm_size,
46         char *mail_alias);
47 FILE *open_sendmail(void);
48 int close_sendmail(FILE *fp);
49
50 int
51 main(int argc, char **argv)
52     {
53     int status = 0;
54     char *delta_file;
55     char *mail_alias;
56     long max_msg_size = DEF_MAX_MSG;
57     long max_ctm_size = 0;
58     char *log_file = NULL;
59     char *queue_dir = NULL;
60     char *delta;
61     FILE *dfp;
62     struct stat sb;
63
64     err_prog_name(argv[0]);
65
66     OPTIONS("[-l log] [-m maxmsgsize] [-c maxctmsize] [-q queuedir] ctm-delta mail-alias")
67         NUMBER('m', max_msg_size)
68         NUMBER('c', max_ctm_size)
69         STRING('l', log_file)
70         STRING('q', queue_dir)
71     ENDOPTS
72
73     if (argc != 3)
74         usage();
75
76     if (log_file != NULL)
77         err_set_log(log_file);
78
79     delta_file = argv[1];
80     mail_alias = argv[2];
81
82     if ((delta = strrchr(delta_file, '/')) == NULL)
83         delta = delta_file;
84     else
85         delta++;
86
87     if ((dfp = fopen(delta_file, "r")) == NULL || fstat(fileno(dfp), &sb) < 0)
88         {
89         err("*%s", delta_file);
90         exit(1);
91         }
92
93     if (max_ctm_size != 0 && sb.st_size > max_ctm_size)
94         status = apologise(delta, sb.st_size, max_ctm_size, mail_alias);
95     else
96         status = chop_and_send_or_queue(dfp, delta, sb.st_size, max_msg_size,
97                 mail_alias, queue_dir);
98
99     fclose(dfp);
100
101     return status;
102     }
103
104
105 /*
106  * Carve our CTM delta into pieces, encode them, and send or queue them.
107  * Returns 0 on success, and 1 on failure.
108  */
109 int
110 chop_and_send_or_queue(FILE *dfp, char *delta, off_t ctm_size,
111         long max_msg_size, char *mail_alias, char *queue_dir)
112     {
113     int npieces;
114     long msg_size;
115     long exp_size;
116     int status;
117
118 #undef howmany
119 #define howmany(x,y)    (((x)+((y)-1)) / (y))
120
121     /*
122      * Work out how many pieces we need, bearing in mind that each piece
123      * grows by 4/3 when encoded.  We count the newlines too, but ignore
124      * all mail headers and piece headers.  They are a "small" (almost
125      * constant) per message overhead that we make the user worry about. :-)
126      */
127     exp_size = ctm_size * 4 / 3;
128     exp_size += howmany(exp_size, LINE_LENGTH);
129     npieces = howmany(exp_size, max_msg_size);
130     msg_size = howmany(ctm_size, npieces);
131
132 #undef howmany
133
134     if (queue_dir == NULL)
135         status = chop_and_send(dfp, delta, msg_size, npieces, mail_alias);
136     else
137         {
138         status = chop_and_queue(dfp, delta, msg_size, npieces, mail_alias,
139                 queue_dir);
140         if (status)
141             clean_up_queue(queue_dir);
142         }
143
144     return status;
145     }
146
147
148 /*
149  * Carve our CTM delta into pieces, encode them, and send them.
150  * Returns 0 on success, and 1 on failure.
151  */
152 int
153 chop_and_send(FILE *dfp, char *delta, long msg_size, int npieces,
154         char *mail_alias)
155     {
156     int pce;
157     FILE *sfp;
158     unsigned sum;
159
160     /*
161      * Send each chunk directly to sendmail as it is generated.
162      * No temporary files necessary.  If things turn ugly, we just
163      * have to live with the fact the we have sent only part of
164      * the delta.
165      */
166     for (pce = 1; pce <= npieces; pce++)
167         {
168         int read_error;
169
170         if ((sfp = open_sendmail()) == NULL)
171             return 1;
172
173         write_header(sfp, mail_alias, delta, pce, npieces);
174         read_error = encode_body(sfp, dfp, msg_size, &sum);
175         if (!read_error)
176             write_trailer(sfp, sum);
177
178         if (!close_sendmail(sfp) || read_error)
179             return 1;
180
181         err("%s %d/%d sent to %s", delta, pce, npieces, mail_alias);
182         }
183
184     return 0;
185     }
186
187
188 /*
189  * Construct the tmp queue file name of a delta piece.
190  */
191 #define mk_tmp_name(fn,qd,p) \
192     sprintf((fn), "%s/.%08ld.%03d", (qd), (long)getpid(), (p))
193
194 /*
195  * Construct the final queue file name of a delta piece.
196  */
197 #define mk_queue_name(fn,qd,d,p,n) \
198     sprintf((fn), "%s/%s+%03d-%03d", (qd), (d), (p), (n))
199
200 /*
201  * Carve our CTM delta into pieces, encode them, and queue them.
202  * Returns 0 on success, and 1 on failure.
203  */
204 int
205 chop_and_queue(FILE *dfp, char *delta, long msg_size, int npieces,
206         char *mail_alias, char *queue_dir)
207     {
208     int pce;
209     FILE *qfp;
210     unsigned sum;
211     char tname[PATH_MAX];
212     char qname[PATH_MAX];
213
214     /*
215      * Store each piece in the queue directory, but under temporary names,
216      * so that they can be deleted without unpleasant consequences if
217      * anything goes wrong.  We could easily fill up a disk, for example.
218      */
219     for (pce = 1; pce <= npieces; pce++)
220         {
221         int write_error;
222
223         mk_tmp_name(tname, queue_dir, pce);
224         if ((qfp = fopen(tname, "w")) == NULL)
225             {
226             err("cannot open '%s' for writing", tname);
227             return 1;
228             }
229
230         write_header(qfp, mail_alias, delta, pce, npieces);
231         if (encode_body(qfp, dfp, msg_size, &sum))
232             return 1;
233         write_trailer(qfp, sum);
234
235         fflush(qfp);
236         write_error = ferror(qfp);
237         fclose(qfp);
238         if (write_error)
239             {
240             err("error writing '%s'", tname);
241             return 1;
242             }
243
244         /*
245          * Give the warm success message now, instead of all in a rush
246          * during the rename phase.
247          */
248         err("%s %d/%d queued for %s", delta, pce, npieces, mail_alias);
249         }
250
251     /*
252      * Rename the pieces into place.  If an error occurs now, we are
253      * stuffed, but there is no neat way to back out.  rename() should
254      * only fail now under extreme circumstances.
255      */
256     for (pce = 1; pce <= npieces; pce++)
257         {
258         mk_tmp_name(tname, queue_dir, pce);
259         mk_queue_name(qname, queue_dir, delta, pce, npieces);
260         if (rename(tname, qname) < 0)
261             {
262             err("*rename: '%s' to '%s'", tname, qname);
263             unlink(tname);
264             }
265         }
266
267     return 0;
268     }
269
270
271 /*
272  * There may be temporary files cluttering up the queue directory.
273  */
274 void
275 clean_up_queue(char *queue_dir)
276     {
277     int pce;
278     char tname[PATH_MAX];
279
280     err("discarding queued delta pieces");
281     for (pce = 1; ; pce++)
282         {
283         mk_tmp_name(tname, queue_dir, pce);
284         if (unlink(tname) < 0)
285             break;
286         }
287     }
288
289
290 /*
291  * MIME BASE64 encode table.
292  */
293 static char to_b64[0x40] =
294     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
295
296 /*
297  * This cheap plastic checksum effectively rotates our checksum-so-far
298  * left one, then adds the character.  We only want 16 bits of it, and
299  * don't care what happens to the rest.  It ain't much, but it's small.
300  */
301 #define add_ck(sum,x)   \
302     ((sum) += ((x)&0xff) + (sum) + (((sum)&0x8000) ? 1 : 0))
303
304 /*
305  * Encode the body.  Use an encoding almost the same as MIME BASE64.
306  *
307  * Characters are read from delta_fp and encoded characters are written
308  * to sm_fp.  At most 'msg_size' characters should be read from delta_fp.
309  *
310  * The body consists of lines of up to LINE_LENGTH characters.  Each group
311  * of 4 characters encodes 3 input characters.  Each output character encodes
312  * 6 bits.  Thus 64 different characters are needed in this representation.
313  */
314 int
315 encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size, unsigned *sum)
316     {
317     unsigned short cksum = 0xffff;
318     unsigned char *ip;
319     char *op;
320     int want, n, i;
321     unsigned char inbuf[LINE_LENGTH*3/4];
322     char outbuf[LINE_LENGTH+1];
323
324     /*
325      * Round up to the nearest line boundary, for the tiniest of gains,
326      * and lots of neatness. :-)
327      */
328     msg_size += (LINE_LENGTH*3/4) - 1;
329     msg_size -= msg_size % (LINE_LENGTH*3/4);
330
331     while (msg_size > 0)
332         {
333         want = (msg_size < sizeof(inbuf)) ? msg_size : sizeof(inbuf);
334         if ((n = fread(inbuf, sizeof(char), want, delta_fp)) == 0)
335             break;
336         msg_size -= n;
337
338         for (i = 0; i < n; i++)
339             add_ck(cksum, inbuf[i]);
340
341         /*
342          * Produce a line of encoded data.  Every line length will be a
343          * multiple of 4, except for, perhaps, the last line.
344          */
345         ip = inbuf;
346         op = outbuf;
347         while (n >= 3)
348             {
349             *op++ = to_b64[ip[0] >> 2];
350             *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4];
351             *op++ = to_b64[(ip[1] << 2 & 0x3f) | ip[2] >> 6];
352             *op++ = to_b64[ip[2] & 0x3f];
353             ip += 3;
354             n -= 3;
355             }
356         if (n > 0)
357             {
358             *op++ = to_b64[ip[0] >> 2];
359             *op++ = to_b64[(ip[0] << 4 & 0x3f) | ip[1] >> 4];
360             if (n >= 2)
361                 *op++ = to_b64[ip[1] << 2 & 0x3f];
362             }
363         *op++ = '\n';
364         fwrite(outbuf, sizeof(char), op - outbuf, sm_fp);
365         }
366
367     if (ferror(delta_fp))
368         {
369         err("error reading input file.");
370         return 1;
371         }
372
373     *sum = cksum;
374
375     return 0;
376     }
377
378
379 /*
380  * Write the mail header and data header.
381  */
382 void
383 write_header(FILE *sfp, char *mail_alias, char *delta, int pce, int npieces)
384     {
385     fprintf(sfp, "From: owner-%s\n", mail_alias);
386     fprintf(sfp, "To: %s\n", mail_alias);
387     fprintf(sfp, "Subject: ctm-mail %s %d/%d\n\n", delta, pce, npieces);
388
389     fprintf(sfp, "CTM_MAIL BEGIN %s %d %d\n", delta, pce, npieces);
390     }
391
392
393 /*
394  * Write the data trailer.
395  */
396 void
397 write_trailer(FILE *sfp, unsigned sum)
398     {
399     fprintf(sfp, "CTM_MAIL END %ld\n", (long)sum);
400     }
401
402
403 /*
404  * We're terribly sorry, but the delta is too big to send.
405  * Returns 0 on success, 1 on failure.
406  */
407 int
408 apologise(char *delta, off_t ctm_size, long max_ctm_size, char *mail_alias)
409     {
410     FILE *sfp;
411
412     sfp = open_sendmail();
413     if (sfp == NULL)
414         return 1;
415
416     fprintf(sfp, "From: owner-%s\n", mail_alias);
417     fprintf(sfp, "To: %s\n", mail_alias);
418     fprintf(sfp, "Subject: ctm-notice %s\n\n", delta);
419
420     fprintf(sfp, "%s is %ld bytes.  The limit is %ld bytes.\n\n", delta,
421         (long)ctm_size, max_ctm_size);
422     fprintf(sfp, "You can retrieve this delta via ftpmail, "
423         "or your good mate at the university.\n");
424
425     if (!close_sendmail(sfp))
426         return 1;
427
428     return 0;
429     }
430
431
432 /*
433  * Start a pipe to sendmail.  Sendmail will decode the destination
434  * from the message contents.
435  */
436 FILE *
437 open_sendmail()
438     {
439     FILE *fp;
440     char buf[100];
441
442     sprintf(buf, "%s -odq -t", _PATH_SENDMAIL);
443     if ((fp = popen(buf, "w")) == NULL)
444         err("cannot start sendmail");
445     return fp;
446     }
447
448
449 /*
450  * Close a pipe to sendmail.  Sendmail will then do its bit.
451  * Return 1 on success, 0 on failure.
452  */
453 int
454 close_sendmail(FILE *fp)
455     {
456     int status;
457
458     fflush(fp);
459     if (ferror(fp))
460         {
461         err("error writing to sendmail");
462         return 0;
463         }
464
465     if ((status = pclose(fp)) != 0)
466         err("sendmail failed with status %d", status);
467
468     return (status == 0);
469     }