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