1
/* ========================================================================
2
* Copyright 1988-2006 University of Washington
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
8
* http://www.apache.org/licenses/LICENSE-2.0
11
* ========================================================================
15
* Program: Mail Delivery Module
17
* Author: Mark Crispin
18
* Networks and Distributed Computing
19
* Computing & Communications
20
* University of Washington
21
* Administration Building, AG-44
23
* Internet: MRC@CAC.Washington.EDU
26
* Last Edited: 22 December 2006
32
extern int errno; /* just in case */
44
char *version = "2006e.16"; /* tmail release version */
45
int debug = NIL; /* debugging (don't fork) */
46
int trycreate = NIL; /* flag saying gotta create before appending */
47
int critical = NIL; /* flag saying in critical code */
48
char *sender = NIL; /* message origin */
49
char *inbox = NIL; /* inbox file */
52
/* Function prototypes */
54
void file_string_init (STRING *s,void *data,unsigned long size);
55
char file_string_next (STRING *s);
56
void file_string_setpos (STRING *s,unsigned long i);
57
int main (int argc,char *argv[]);
58
int deliver (FILE *f,unsigned long msglen,char *user);
59
long ibxpath (MAILSTREAM *ds,char **mailbox,char *path);
60
int deliver_safely (MAILSTREAM *prt,STRING *st,char *mailbox,char *path,
62
int delivery_unsafe (char *path,uid_t uid,struct stat *sbuf,char *tmp);
63
int fail (char *string,int code);
64
char *getusername (char *s,char **t);
67
/* File string driver for file stringstructs */
69
STRINGDRIVER file_string = {
70
file_string_init, /* initialize string structure */
71
file_string_next, /* get next byte in string structure */
72
file_string_setpos /* set position in string structure */
76
/* Cache buffer for file stringstructs */
78
#define CHUNKLEN 16384
81
/* Initialize file string structure for file stringstruct
82
* Accepts: string structure
87
void file_string_init (STRING *s,void *data,unsigned long size)
89
s->data = data; /* note fd */
90
s->size = size; /* note size */
92
s->chunksize = (unsigned long) CHUNKLEN;
93
SETPOS (s,0); /* set initial position */
97
/* Get next character from file stringstruct
98
* Accepts: string structure
99
* Returns: character, string structure chunk refreshed
102
char file_string_next (STRING *s)
104
char c = *s->curpos++; /* get next byte */
105
SETPOS (s,GETPOS (s)); /* move to next chunk */
106
return c; /* return the byte */
110
/* Set string pointer position for file stringstruct
111
* Accepts: string structure
115
void file_string_setpos (STRING *s,unsigned long i)
117
if (i > s->size) i = s->size; /* don't permit setting beyond EOF */
118
s->offset = i; /* set new offset */
119
s->curpos = s->chunk; /* reset position */
120
/* set size of data */
121
if (s->cursize = min (s->chunksize,SIZE (s))) {
122
/* move to that position in the file */
123
fseek ((FILE *) s->data,s->offset,SEEK_SET);
124
fread (s->curpos,sizeof (char),(unsigned int) s->cursize,(FILE *) s->data);
130
int main (int argc,char *argv[])
134
unsigned long msglen,status = 0;
135
char *s,tmp[MAILTMPLEN];
136
uid_t ruid = getuid ();
138
openlog ("tmail",LOG_PID,LOG_MAIL);
140
/* make sure have some arguments */
141
if (--argc < 1) _exit (fail ("usage: tmail [-D] user[+folder]",EX_USAGE));
142
/* process all flags */
143
while (argc && (*(s = *++argv)) == '-') {
144
argc--; /* gobble this argument */
145
switch (s[1]) { /* what is this flag? */
146
case 'D': /* debug */
147
debug = T; /* don't fork */
149
case 'd': /* obsolete flag meaning multiple users */
151
case 'I': /* inbox specifier */
152
if (argc--) inbox = cpystr (*++argv);
153
else _exit (fail ("missing argument to -I",EX_USAGE));
155
case 'f': /* new name for this flag */
156
case 'r': /* flag giving return path */
157
if (argc--) sender = cpystr (*++argv);
158
else _exit (fail ("missing argument to -r",EX_USAGE));
160
default: /* anything else */
161
_exit (fail ("unknown switch",EX_USAGE));
165
if (!argc) ret = fail ("no recipients",EX_USAGE);
166
else if (!(f = tmpfile ())) ret = fail ("can't make temp file",EX_TEMPFAIL);
167
else { /* build delivery headers */
168
if (sender) fprintf (f,"Return-Path: <%s>\015\012",sender);
169
/* start Received line: */
170
fprintf (f,"Received: via tmail-%s",version);
171
/* not root or daemon? */
172
if (ruid && !((pwd = getpwnam ("daemon")) && (ruid == pwd->pw_uid))) {
173
pwd = getpwuid (ruid); /* get unprivileged user's information */
175
if (pwd) sprintf (tmp,"user %.80s",pwd->pw_name);
176
else sprintf (tmp,"UID %ld",(long) ruid);
177
strcat (tmp," is not privileged to use -I");
178
_exit (fail (tmp,EX_USAGE));
180
fputs (" (invoked by ",f);
181
if (pwd) fprintf (f,"user %s",pwd->pw_name);
182
else fprintf (f,"UID %ld",(long) ruid);
185
/* write "for" if single recipient */
186
if (argc == 1) fprintf (f," for %s",*argv);
190
fputs ("\015\012",f);
191
/* copy text from standard input */
192
if (!fgets (tmp,MAILTMPLEN-1,stdin) || !(s = strchr (tmp,'\n')) ||
193
(s == tmp) || s[1]) _exit (fail ("bad first message line",EX_USAGE));
194
if (s[-1] == '\015') { /* nuke leading "From " line */
195
if ((tmp[0] != 'F') || (tmp[1] != 'r') || (tmp[2] != 'o') ||
196
(tmp[3] != 'm') || (tmp[4] != ' ')) fputs (tmp,f);
197
while ((c = getchar ()) != EOF) putc (c,f);
200
mm_log ("tmail called with LF-only newlines",WARN);
201
if ((tmp[0] != 'F') || (tmp[1] != 'r') || (tmp[2] != 'o') ||
202
(tmp[3] != 'm') || (tmp[4] != ' ')) {
203
*s++ = '\015'; /* overwrite NL with CRLF */
205
*s = '\0'; /* tie off string */
206
fputs (tmp,f); /* write line */
208
/* copy text from standard input */
209
while ((c = getchar ()) != EOF) {
210
/* add CR if needed */
211
if (c == '\012') putc ('\015',f);
215
msglen = ftell (f); /* size of message */
216
fflush (f); /* make sure all changes written out */
218
if (ferror (f)) ret = fail ("error writing temp file",EX_TEMPFAIL);
219
else if (!msglen) ret = fail ("empty message",EX_TEMPFAIL);
220
/* single delivery */
221
else if (argc == 1) ret = deliver (f,msglen,*argv);
222
else do { /* multiple delivery uses daughter forks */
223
if ((pid = fork ()) < 0) ret = fail (strerror (errno),EX_OSERR);
224
else if (pid) { /* mother process */
225
grim_pid_reap_status (pid,NIL,(void *) status);
226
/* normal termination? */
227
if (!ret) ret = (status & 0xff) ? EX_SOFTWARE : (status & 0xff00) >> 8;
229
/* daughter process */
230
else _exit (deliver (f,msglen,*argv));
231
} while (--argc && *argv++);
232
mm_dlog (ret ? "error in delivery" : "all recipients delivered");
234
if (f) fclose (f); /* all done with temporary file */
235
_exit (ret); /* normal exit */
236
return 0; /* stupid gcc */
239
/* Deliver message to recipient list
240
* Accepts: file description of message temporary file
241
* size of message temporary file in bytes
243
* Returns: NIL if success, else error code
246
int deliver (FILE *f,unsigned long msglen,char *user)
248
MAILSTREAM *ds = NIL;
250
char *s,*t,*mailbox,tmp[MAILTMPLEN],path[MAILTMPLEN];
255
uid_t euid = geteuid ();
256
/* get user record */
257
if (!(pwd = getpwnam (getusername (user,&mailbox)))) {
258
sprintf (tmp,"no such user as %.80s",user);
259
return fail (tmp,EX_NOUSER);
261
/* absurd is absurd */
262
if (mailbox && (strlen (mailbox) > 256))
263
return fail ("absurd folder name",EX_NOUSER);
264
/* big security hole if this is allowed */
265
if (!(duid = pwd->pw_uid)) return fail ("mail to root prohibited",EX_NOUSER);
266
/* log in as user if different than euid */
267
if ((duid != euid) && !loginpw (pwd,1,&user)) {
268
sprintf (tmp,"unable to log in UID %ld from UID %ld",
269
(long) duid,(long) euid);
270
return fail (tmp,EX_NOUSER);
272
/* can't use pwd after this point */
273
env_init (pwd->pw_name,pwd->pw_dir);
274
sprintf (tmp,"delivering to %.80s+%.80s",user,mailbox ? mailbox : "INBOX");
276
/* prepare stringstruct */
277
INIT (&st,file_string,(void *) f,msglen);
278
if (mailbox) { /* non-INBOX name */
279
switch (mailbox[0]) { /* make sure a valid name */
280
default: /* other names, try to deliver if not INBOX */
281
if (!strstr (mailbox,"..") && !strstr (mailbox,"//") &&
282
!strstr (mailbox,"/~") && mailboxfile (path,mailbox) && path[0] &&
283
!deliver_safely (NIL,&st,mailbox,path,duid,tmp)) return NIL;
284
case '%': case '*': /* wildcards not valid */
285
case '#': /* namespace name not valid */
286
case '/': /* absolute path names not valid */
287
case '~': /* user names not valid */
288
sprintf (tmp,"invalid mailbox name %.80s+%.80s",user,mailbox);
292
mm_dlog ("retrying delivery to INBOX");
293
SETPOS (&st,0); /* rewind stringstruct just in case */
296
/* -I specified and not "-I INBOX"? */
297
if (inbox && !(((inbox[0] == 'I') || (inbox[0] == 'i')) &&
298
((inbox[1] == 'N') || (inbox[1] == 'n')) &&
299
((inbox[2] == 'B') || (inbox[2] == 'b')) &&
300
((inbox[3] == 'O') || (inbox[3] == 'o')) &&
301
((inbox[4] == 'X') || (inbox[4] == 'x')) && !inbox[5])) {
302
/* "-I #driver.xxx/name"? */
303
if ((*inbox == '#') && ((inbox[1] == 'd') || (inbox[1] == 'D')) &&
304
((inbox[2] == 'r') || (inbox[2] == 'R')) &&
305
((inbox[3] == 'i') || (inbox[3] == 'I')) &&
306
((inbox[4] == 'v') || (inbox[4] == 'V')) &&
307
((inbox[5] == 'e') || (inbox[5] == 'E')) &&
308
((inbox[6] == 'r') || (inbox[6] == 'R')) && (inbox[7] == '.') &&
309
(s = strchr (inbox+8,'/'))) {
310
*s = '\0'; /* temporarily tie off driver name */
311
if (!((dv = mail_parameters (NIL,GET_DRIVER,(void *) (inbox+8))) &&
312
(mailboxfile (path,s[1] ? s + 1 : "&&&&&") == path) &&
313
(s[1] || ((t = strstr (path,"&&&&&")) && strcpy (t,"INBOX"))))) {
314
path[0] = '\0'; /* bad -I argument, no path resolved */
315
sprintf (tmp,"Unable to resolve driver in %.80s, -I ignored",inbox);
318
*s = '/'; /* restore delimiter */
320
/* resolve "-I other" specification */
321
else if (mailboxfile (path,inbox) && path[0]) {
322
/* resolution succeeded, impute driver */
323
if (!strcmp (inbox,"mail.txt"))
324
dv = mail_parameters (NIL,GET_DRIVER,(void *) "tenex");
325
else if (!strcmp (inbox,"INBOX.MTX"))
326
dv = mail_parameters (NIL,GET_DRIVER,(void *) "mtx");
327
else if (!strcmp (inbox,"mbox"))
328
dv = mail_parameters (NIL,GET_DRIVER,(void *) "unix");
330
else { /* bad -I argument */
331
path[0] = '\0'; /* no path resolved */
332
sprintf (tmp,"Unable to resolve %.80s, -I ignored",inbox);
335
if (*path) { /* -I successfully resolved a path? */
339
/* supplicate to the Evil One if necessary */
340
if (lstat (path,&sbuf) && !path_create (ds,path)) {
341
/* the Evil One rejected the plea */
342
sprintf (tmp,"Unable to create %.80s, -I ignored",path);
345
/* now attempt delivery */
346
else return deliver_safely (ds,&st,inbox,path,duid,tmp);
350
/* no -I, resolve "INBOX" into path */
351
if (mailboxfile (path,mailbox = "INBOX") && !path[0]) {
352
/* clear box, get generic INBOX prototype */
353
if (!(ds = mail_open (NIL,"INBOX",OP_PROTOTYPE)))
354
fatal ("no INBOX prototype");
355
/* standard system driver? */
356
if (!strcmp (ds->dtb->name,"unix") || !strcmp (ds->dtb->name,"mmdf")) {
357
strcpy (path,sysinbox ());/* use system INBOX */
358
if (!lstat (path,&sbuf)) /* deliver to existing system INBOX */
359
return deliver_safely (ds,&st,mailbox,path,duid,tmp);
361
else { /* other driver, try ~/INBOX */
362
if ((mailboxfile (path,"&&&&&") == path) &&
363
(s = strstr (path,"&&&&&")) && strcpy (s,"INBOX") &&
364
!lstat (path,&sbuf)){ /* deliver to existing ~/INBOX */
365
sprintf (tmp,"#driver.%s/INBOX",ds->dtb->name);
366
return deliver_safely (ds,&st,cpystr (tmp),path,duid,tmp);
369
/* not dummy, deliver to driver imputed path */
370
if (strcmp (ds->dtb->name,"dummy"))
371
return (ibxpath (ds,&mailbox,path) && !lstat (path,&sbuf)) ?
372
deliver_safely (ds,&st,mailbox,path,duid,tmp) :
373
fail ("unable to resolve INBOX path",EX_CANTCREAT);
374
/* dummy, empty imputed append path exist? */
375
if (ibxpath (ds = default_proto (T),&mailbox,path) &&
376
!lstat (path,&sbuf) && !sbuf.st_size)
377
return deliver_safely (ds,&st,mailbox,path,duid,tmp);
378
/* impute path that we will create */
379
if (!ibxpath (ds = default_proto (NIL),&mailbox,path))
380
return fail ("unable to resolve INBOX",EX_CANTCREAT);
382
/* black box, must create, get create proto */
383
else if (lstat (path,&sbuf)) ds = default_proto (NIL);
384
else { /* black box, existing file */
385
/* empty file, get append prototype */
386
if (!sbuf.st_size) ds = default_proto (T);
387
/* non-empty, get prototype from its data */
388
else if (!(ds = mail_open (NIL,"INBOX",OP_PROTOTYPE)))
389
fatal ("no INBOX prototype");
390
/* error if unknown format */
391
if (!strcmp (ds->dtb->name,"phile"))
392
return fail ("unknown format INBOX",EX_UNAVAILABLE);
393
/* otherwise can deliver to it */
394
return deliver_safely (ds,&st,mailbox,path,duid,tmp);
396
sprintf (tmp,"attempting to create mailbox %.80s path %.80s",mailbox,path);
398
/* supplicate to the Evil One */
399
if (!path_create (ds,path)) return fail ("can't create INBOX",EX_CANTCREAT);
400
sprintf (tmp,"created %.80s",path);
402
/* deliver the message */
403
return deliver_safely (ds,&st,mailbox,path,duid,tmp);
406
/* Resolve INBOX from driver prototype into mailbox name and filesystem path
407
* Accepts: driver prototype
408
* pointer to mailbox name string pointer
409
* buffer to return mailbox path
410
* Returns: T if success, NIL if error
413
long ibxpath (MAILSTREAM *ds,char **mailbox,char *path)
415
char *s,tmp[MAILTMPLEN];
417
if (!strcmp (ds->dtb->name,"unix") || !strcmp (ds->dtb->name,"mmdf"))
418
strcpy (path,sysinbox ()); /* use system INBOX for unix and MMDF */
419
else if (!strcmp (ds->dtb->name,"tenex"))
420
ret = (mailboxfile (path,"mail.txt") == path) ? T : NIL;
421
else if (!strcmp (ds->dtb->name,"mtx"))
422
ret = (mailboxfile (path,"INBOX.MTX") == path) ? T : NIL;
423
else if (!strcmp (ds->dtb->name,"mbox"))
424
ret = (mailboxfile (path,"mbox") == path) ? T : NIL;
425
/* better not be a namespace driver */
426
else if (ds->dtb->flags & DR_NAMESPACE) return NIL;
427
/* INBOX in home directory */
428
else ret = ((mailboxfile (path,"&&&&&") == path) &&
429
(s = strstr (path,"&&&&&")) && strcpy (s,"INBOX")) ? T : NIL;
430
if (ret) { /* don't bother if lossage */
431
sprintf (tmp,"#driver.%s/INBOX",ds->dtb->name);
432
*mailbox = cpystr (tmp); /* name of INBOX in this namespace */
438
* Accepts: prototype stream to force mailbox format
439
* stringstruct of message temporary file or NIL for check only
441
* filesystem path name
443
* scratch buffer for messages
444
* Returns: NIL if success, else error code
447
int deliver_safely (MAILSTREAM *prt,STRING *st,char *mailbox,char *path,
451
int i = delivery_unsafe (path,uid,&sbuf,tmp);
452
if (i) return i; /* give up now if delivery unsafe */
453
/* directory, not file */
454
if ((sbuf.st_mode & S_IFMT) == S_IFDIR) {
455
if (sbuf.st_mode & 0001) { /* listable directories may be worrisome */
456
sprintf (tmp,"WARNING: directory %.80s is listable",path);
460
else { /* file, not directory */
461
if (sbuf.st_nlink != 1) { /* multiple links may be worrisome */
462
sprintf (tmp,"WARNING: multiple links to file %.80s",path);
465
if (sbuf.st_mode & 0111) { /* executable files may be worrisome */
466
sprintf (tmp,"WARNING: file %.80s is executable",path);
470
if (sbuf.st_mode & 0002) { /* public-write files may be worrisome */
471
sprintf (tmp,"WARNING: file %.80s is publicly-writable",path);
474
if (sbuf.st_mode & 0004) { /* public-write files may be worrisome */
475
sprintf (tmp,"WARNING: file %.80s is publicly-readable",path);
478
/* so far, so good */
479
sprintf (tmp,"%s appending to %.80s (%s %.80s)",
480
prt ? prt->dtb->name : "default",mailbox,
481
((sbuf.st_mode & S_IFMT) == S_IFDIR) ? "directory" : "file",path);
483
/* do the append now! */
484
if (!mail_append (prt,mailbox,st)) {
485
sprintf (tmp,"message delivery failed to %.80s",path);
486
return fail (tmp,EX_CANTCREAT);
489
sprintf (tmp,"delivered to %.80s",path);
491
/* make sure nothing evil this way comes */
492
return delivery_unsafe (path,uid,&sbuf,tmp);
495
/* Verify that delivery is safe
499
* scratch buffer for messages
500
* Returns: NIL if delivery is safe, error code if unsafe
503
int delivery_unsafe (char *path,uid_t uid,struct stat *sbuf,char *tmp)
506
sprintf (tmp,"Verifying safe delivery to %.80s by UID %ld",path,(long) uid);
508
/* prepare message just in case */
509
sprintf (tmp,"delivery to %.80s unsafe: ",path);
510
/* unsafe if can't get its status */
511
if (lstat (path,sbuf)) strcat (tmp,strerror (errno));
512
else if (sbuf->st_uid != uid) /* unsafe if UID does not match */
513
sprintf (tmp + strlen (tmp),"uid mismatch (%ld != %ld)",
514
(long) sbuf->st_uid,(long) uid);
515
/* unsafe if not a regular file */
516
else if (((type = sbuf->st_mode & (S_IFMT | S_ISUID | S_ISGID)) != S_IFREG)&&
518
strcat (tmp,"can't deliver to ");
519
/* unsafe if setuid */
520
if (type & S_ISUID) strcat (tmp,"setuid file");
521
/* unsafe if setgid */
522
else if (type & S_ISGID) strcat (tmp,"setgid file");
524
case S_IFCHR: strcat (tmp,"character special"); break;
525
case S_IFBLK: strcat (tmp,"block special"); break;
526
case S_IFLNK: strcat (tmp,"symbolic link"); break;
527
case S_IFSOCK: strcat (tmp,"socket"); break;
529
sprintf (tmp + strlen (tmp),"file type %07o",(unsigned int) type);
532
else return NIL; /* OK to deliver */
533
return fail (tmp,EX_CANTCREAT);
537
* Accepts: string to output
540
int fail (char *string,int code)
542
mm_log (string,ERROR); /* pass up the string */
551
code = EX_TEMPFAIL; /* coerce these to TEMPFAIL */
556
return code; /* error code to return */
560
/* Get user name from username+mailbox specifier
561
* Accepts: username/mailbox specifier
562
* pointer to return location for mailbox specifier
563
* Returns: user name, mailbox specifier value NIL if INBOX, patches out +
566
char *getusername (char *s,char **t)
568
char tmp[MAILTMPLEN];
569
if (*t = strchr (s,'+')) { /* have a mailbox specifier? */
570
*(*t)++ = '\0'; /* yes, tie off user name */
571
/* user+ and user+INBOX same as user */
572
if (!**t || !strcmp ("INBOX",ucase (strcpy (tmp,*t)))) *t = NIL;
574
return s; /* return user name */
577
/* Co-routines from MAIL library */
580
/* Message matches a search
581
* Accepts: MAIL stream
585
void mm_searched (MAILSTREAM *stream,unsigned long msgno)
587
fatal ("mm_searched() call");
591
/* Message exists (i.e. there are that many messages in the mailbox)
592
* Accepts: MAIL stream
596
void mm_exists (MAILSTREAM *stream,unsigned long number)
598
fatal ("mm_exists() call");
603
* Accepts: MAIL stream
607
void mm_expunged (MAILSTREAM *stream,unsigned long number)
609
fatal ("mm_expunged() call");
613
/* Message flags update seen
614
* Accepts: MAIL stream
618
void mm_flags (MAILSTREAM *stream,unsigned long number)
623
* Accepts: MAIL stream
629
void mm_list (MAILSTREAM *stream,int delimiter,char *name,long attributes)
631
fatal ("mm_list() call");
635
/* Subscribed mailbox found
636
* Accepts: MAIL stream
642
void mm_lsub (MAILSTREAM *stream,int delimiter,char *name,long attributes)
644
fatal ("mm_lsub() call");
649
* Accepts: MAIL stream
654
void mm_status (MAILSTREAM *stream,char *mailbox,MAILSTATUS *status)
656
fatal ("mm_status() call");
659
/* Notification event
660
* Accepts: MAIL stream
665
void mm_notify (MAILSTREAM *stream,char *string,long errflg)
667
char tmp[MAILTMPLEN];
668
tmp[11] = '\0'; /* see if TRYCREATE */
669
if (!strcmp (ucase (strncpy (tmp,string,11)),"[TRYCREATE]")) trycreate = T;
670
mm_log (string,errflg); /* just do mm_log action */
674
/* Log an event for the user to see
675
* Accepts: string to log
679
void mm_log (char *string,long errflg)
681
if (trycreate)mm_dlog(string);/* debug logging only if trycreate in effect */
682
else { /* ordinary logging */
683
fprintf (stderr,"%s\n",string);
685
case NIL: /* no error */
686
syslog (LOG_INFO,"%s",string);
688
case PARSE: /* parsing problem */
689
case WARN: /* warning */
690
syslog (LOG_WARNING,"%s",string);
692
case ERROR: /* error */
694
syslog (LOG_ERR,"%s",string);
701
/* Log an event to debugging telemetry
702
* Accepts: string to log
705
void mm_dlog (char *string)
707
if (debug) fprintf (stderr,"%s\n",string);
708
syslog (LOG_DEBUG,"%s",string);
711
/* Get user name and password for this host
712
* Accepts: parse of network mailbox name
713
* where to return user name
714
* where to return password
718
void mm_login (NETMBX *mb,char *username,char *password,long trial)
720
fatal ("mm_login() call");
724
/* About to enter critical code
728
void mm_critical (MAILSTREAM *stream)
730
critical = T; /* note in critical code */
734
/* About to exit critical code
738
void mm_nocritical (MAILSTREAM *stream)
740
critical = NIL; /* note not in critical code */
747
* flag indicating that mailbox may be clobbered
748
* Returns: T if user wants to abort
751
long mm_diskerror (MAILSTREAM *stream,long errcode,long serious)
757
/* Log a fatal error event
758
* Accepts: string to log
761
void mm_fatal (char *string)
763
printf ("?%s\n",string); /* shouldn't happen normally */