ftp.nice.ch/pub/next/unix/mail/n2m.1.3.s.tar.gz#/n2m/n2m.c

This is n2m.c in view mode; [Download] [Up]

/*
 * n2m -- translate NeXT multimedia message to MIME
 */
#include <stdio.h>
#include <limits.h>
#include <sys/types.h> /* For sys/stat, below. */
#include <sys/stat.h>  /* For file checking. */
#include <signal.h>    /* For the timer and signals. */
#include <setjmp.h>
#include <strings.h>

#include "strings.h"   /* Local additions. */
#include "n2m.h"
#include "patchlevel.h"

char *Version = VERSION;        /* The version of the program. */
char *ProgName = NULL;          /* It's name. */
jmp_buf TimeoutJmpBuf,		/* Timeout waiting for input from network. */
	InterruptJmpBuf;	/* Hangup or ^C. */
extern  char *contentType(), *transferEncoding();
int     Verbose = NO;
int     CleanUp = YES;
char	*TmpDir = NULL;
char	*Annotation =  
"The following is NeXT mail translated to MIME format with n2m.\n\
This always starts with the text of the message in ascii and\n\
then in Microsoft's proprietary language, rtf. Each contains\n\
references to attachements, which follow the text message.\n";

#define MAXWAIT	(60*999) /* (60*3)		/* The time to wait: heuristic. */
#define BOUNDARY "n2m-MIME-boundary-------------n2m-MIME-boundary"
#define INNERBOUNDARY "n2m"
#define base64(from,to) to64(from,to)

/*
 * main -- 
 */
main(argc,argv) int argc; char *argv[]; {
	char **newArgV, **doOptions();
	int  i;

	ProgName = argv[0];

	(void) signal(SIGTTIN,SIG_IGN);
	(void) signal(SIGTTOU,SIG_IGN);
	(void) umask(077);
	newArgV = doOptions(argc,argv); 
	if (newArgV[1] == NULL) {
		n2m("stdin");
	}
	else {
		for (i=1; newArgV[i] != NULL; i++) {
			n2m(newArgV[i]);
		}
	}
	exit(0);
	/*NOTREACHED*/
}

/*
 * doOptions -- pull the options out of the command-line, process them 
 *      and return the rest.
 */
 char **
doOptions(argc,argv) int argc; char *argv[]; {
	int    i, 
		newArgC;
	static char *newArgV[MAXARGS];

	newArgV[0] = argv[0];
	newArgC = 1;

	for (i=1; i < argc; i++) {
		if (argv[i][0] != '-') {
			newArgV[newArgC++] = argv[i];
			continue;
		}
		switch(argv[i][1]) {
		case 'd': /* Save the temp files for manual intervention. */
			CleanUp = NO;
			/* Fall into verbose. */
		case 'v': /* Set verbose flag. */
			Verbose = YES;
			break;
		case 'V': /*  Say version and quit. Undocumented. */
			die("n2m version %s",Version);
			break;
		}
	}
	newArgV[newArgC] = NULL;
	return &newArgV[0];
}


/*
 * n2m -- translate the message (exactly one).
 */
n2m(name) char *name;  {
	extern int alarmHandler(), interruptHandler();

	(void) signal(SIGALRM,alarmHandler); /* Catch SIGALRMs. */ 
	(void) signal(SIGHUP,interruptHandler);
	(void) signal(SIGINT,interruptHandler);

	(void) alarm((unsigned)MAXWAIT); /* Set initial timer. */
	if (setjmp(TimeoutJmpBuf) != 0) {
		/* Then the timer has gone off and we bail out. */
		die("the translation took FAR too long: giving up");	
	}
	if (setjmp(InterruptJmpBuf) != 0) {
		die("Interrupted");
	}
	if (Verbose) {
		(void) fprintf(stderr,"%s: Started processing %s\n",
			       ProgName,name);
	}

	makeTempDir();
	decompose(name); /* Turn the message into its components. */
	translate();
	recompose(); /* Put the parts back into a message. */
	removeTempDir();  

	(void) signal(SIGALRM,SIG_IGN); 
	if (Verbose) {
		(void) fprintf(stderr,"Finished processing %s\n",name);
	}
	/* Close conection. */
}


/*
 * decompose -- turn the message into a header file (called
 *	``.0'', and zero or more files from a tar container
 */
decompose(name) char *name; { 
	FILE *mailFp, *headerFp, *bodyFp;
	char buffer[MAXLINE];

	if (strcmp(name,"stdin")==0) {
		mailFp = stdin;
	}
	else if ((mailFp= fopen(name,"r")) == NULL) {
		die("can't open input file %s",name);
	}
	if (chdir(TmpDir) != 0) {
		die("can't change to temp directory %s",TmpDir);
	}
	if ((headerFp= fopen(".0","w")) == NULL) {
		die("can't write to temp file %s/.0.\n",TmpDir);
	}
	if (Verbose) {
		(void) fprintf(stderr,"Writing headers of %s to %s/.0\n",
			       name,TmpDir);
	}

	/* Read until a blank line, except for NeXT funny header. */
	/*  X- those.  Do not write blank line yet. */
	(void) alarm((unsigned)MAXWAIT); /* Set header timer. */
	while (fgets(buffer,sizeof(buffer),mailFp) != NULL) {
		if (blankLine(buffer))
			break;
		else if (funnyNextHeader(buffer)) 
			(void) fputs("X-",headerFp);
		(void) fputs(buffer,headerFp);
		/* (void) fputs(buffer,stdout); */
	}
	(void) alarm((unsigned)MAXWAIT); /* Set contents timer. */
	if ((bodyFp= popen("uudecode","w")) == NULL) {
		die("can't open pipe to uudecode");
	}
	else if (Verbose) {
		(void) fprintf(stderr,"Piping body to uudecode.\n");
	}
	while (fgets(buffer,sizeof(buffer),mailFp) != NULL) {
		(void) fputs(buffer,bodyFp);
		/* (void) fputs(buffer,stdout); */
	}
	if (pclose(bodyFp) != 0) {
		die("pipe to uudecode failed.");
	}
	(void) fclose(mailFp);

	/* Ok, we're done with the input file, decompose the temp files. */
	(void) alarm((unsigned)MAXWAIT); /* Set decompression timer. */
	if (system("chmod u+rw .tar*") != 0) {
	        die("can't fix uudecoded .tar file's mode");
	}
	if (system("cat .tar* | uncompress -c >tempFile.tar") != 0) {
		die("can't uncompress uudecoded .tar file");
	}
	if (system("tar xf tempFile.tar") != 0) {
		die("can't get enough space in %s to extract tar files",
		    TmpDir);
	}
	if (system("tar tf tempFile.tar >.-1") != 0) {
		die("can't get enough space to build an index of the tar file");
	}
	(void) fclose(headerFp);

}

blankLine(line) char *line; {
	register char ch;
	while ((ch= *line++) != '\n') {
		if (ch != ' ' && ch != '\t')
			return NO;
	}
	return YES;
}
funnyNextHeader(line) char *line; {
	return strncmp(line,"Next-Attachment",15) == 0;
}	


/*
 * translate -- translate files in the message, including
 *	index.rtf...
 */
translate() {
	extern char *TmpDir;
	FILE	*inFp, *outFp, 
		*indexFp; /* Reserved for future use. */
	char	iBuf[MAXLINE], 
		dBuf[MAXLINE]; /* Reserved for future use. */


	/* Translate headers. */
		/* TBD */

	/* Translate index.rtf into file ``.1'' */
	if ((inFp= fopen("index.rtf","r")) == NULL) {
		die("can't open %s/index.rtf",TmpDir);
	}
	else if ((outFp= fopen(".1","w")) == NULL) {
		die("can't open %s/.1",TmpDir);
	}
	(void) fprintf(outFp,"Content-Type: multipart/alternative; boundary=%s\n",
		       INNERBOUNDARY);
	(void) fprintf(outFp,"\n--%s\n",INNERBOUNDARY);
	(void) fprintf(outFp,"Content-Type: text/plain; name=\"index.rtf\"\n");
	(void) fprintf(outFp,"\n"); /* The blank line. */
	rtfShred(inFp,outFp);

	(void) fprintf(outFp,"\n--%s\n",INNERBOUNDARY);
	(void) fprintf(outFp,"Content-Type: application/octet-stream; type=microsoft-rtf; name=\"index.rtf\"\n");
	(void) fprintf(outFp,"Content-Transfer-Encoding: 7bit\n");
	(void) fprintf(outFp,"\n"); /* The blank line. */

	(void) rewind(inFp); /* Can't fail? */
	/* The rtf, raw. */
	while (fgets(iBuf,sizeof(iBuf),inFp) != NULL) {
		(void) fputs(iBuf,outFp);
	}
	(void) fprintf(outFp,"\n--%s--\n",INNERBOUNDARY);
	(void) fclose(inFp);
	if (fclose(outFp) == ERR) {
		die("can't close intermediate file \"%s/.1\".",TmpDir);
	}

	/* for each file in \\.-1'', translate if rqd. */
#ifdef VERSION_TWO
	/* Now do everything mentioned in indexFile. */
	if ((indexFp= fopen(".-1","r")) == NULL) {
		die("can't open index of tar file");
	}
	while (fgets(iBuf,sizeof(iBuf),indexFp) != NULL) {
		char	*p,*strchr();

		if ((p=strchr(iBuf,'\n')) != NULL) {
			*p = '\0'; /* can't fail... */
		}
		if ((inFp= fopen(iBuf,"r")) == NULL) {
			die("can't open enclosure %s",iBuf);
		}
		else if (Verbose) {
			(void) fprintf(stderr,"Translating insertion %s\n",
				       iBuf);
		}
		characterize(iBuf,inFp);
		/* Do translation if rqd: TBD */
		(void) fclose(inFp);
	}
	(void) fclose(indexFp);
#endif

}


/*
 * recompose -- put a NeXT mail message back together
 *	again, from the translated/annotated parts. Do the
 *	content-encoding to base64 if necessary.
 */
recompose() {
	FILE	*inFp, *indexFp;
	char	iBuf[MAXLINE], dBuf[MAXLINE];

	if ((inFp= fopen(".0","r")) == NULL) {
		die("can't open headers file \"%s/.0\"",TmpDir);
	}
	else if (Verbose) {
		(void) fprintf(stderr,"Recomposing, starting with headers\n");
	}
	(void) alarm((unsigned)MAXWAIT); /* Set initial timer. */
	while (fgets(dBuf,sizeof(dBuf),inFp) != NULL) {
		(void) fputs(dBuf,stdout);
	}
	(void) fclose(inFp);
	(void) fprintf(stdout,"MIME-Version: 1.0\n");
	(void) fprintf(stdout,"Content-Type: multipart/mixed; boundary=%s\n",
		       BOUNDARY);
	(void) fprintf(stdout,"\n"); /* The blank line. */
	(void)fprintf(stdout,"%s\n",Annotation); /* Stuff about n2m. */

	if ((inFp= fopen(".1","r")) == NULL) {
		die("can't open message file \"%s/.1\"",TmpDir);
	}
	(void) alarm((unsigned)MAXWAIT); /* Set initial timer. */
	(void) fprintf(stdout,"--%s\n",BOUNDARY);
	while (fgets(dBuf,sizeof(dBuf),inFp) != NULL) {
		(void) fputs(dBuf,stdout);
	}
	(void) fclose(inFp);

	/* Now do everything mentioned in indexFile. */
	if ((indexFp= fopen(".-1","r")) == NULL) {
		die("can't open index of tar file");
	}
	while (fgets(iBuf,sizeof(iBuf),indexFp) != NULL) {
		char	*p,*strchr();

		if ((p=strchr(iBuf,'\n')) != NULL) {
			*p = '\0'; /* can't fail... */
		}
		if (strncmp(iBuf,"index.rtf",9)== 0) {
			continue;
		}
		if ((inFp= fopen(iBuf,"r")) == NULL) {
			die("can't open enclosure %s",iBuf);
		}
		else if (Verbose) {
			(void) fprintf(stderr,"Recomposing insertion %s\n",
				       iBuf);
		}

		(void) fprintf(stdout,"\n--%s\n",BOUNDARY);
		characterize(iBuf,inFp);
		(void) fprintf(stdout,"Content-Type: %s; name=\"%s\"\n",
			       contentType(),iBuf);
		(void) fprintf(stdout,"Content-Transfer-Encoding: %s\n",
			       transferEncoding());
		(void) fprintf(stdout,"\n"); /* The blank line. */

		if (strncmp(transferEncoding(),"base64",6)==0) {
			if (Verbose) {
				(void) fprintf(stderr,"Encoding %s in base64\n",
					iBuf);
			}
			base64(inFp,stdout);
		}
		else {
			/* It's plain text of some sort (on a NeXT!) */
			while (fgets(dBuf,sizeof(dBuf),inFp) != NULL) {
				(void) fputs(dBuf,stdout);
			}
		}
		(void) fclose(inFp);
	}
	(void) fclose(indexFp);
	(void) fprintf(stdout,"\n--%s--\n",BOUNDARY);
}


/*
** class characterize -- generate a Content-type and Transfer-encoding
**	by reading the file's extension.  Works well for NeXTs and
**	PCs, generally insufficient. This is mainly done to detect
**	files that can be left in 7-bit ascii without base64ification.
**	Plus other things that aren't application/octet-stream.
**	Functions are characterize, contentType and transferEncoding.
**	Constraints are (characterize*,[contentType|transferEncoding]*)
*/
struct exttable_t {
	char	*ext;
	char	*content_type;
	char	*encoding;
} ExtTable[] = {
{ ".vox",	"audio/basic",	/* This isn't quite right... */	"base64" },
{ ".txt",	"text/plain",					"7bit" },
{ ".text",	"text/plain",					"7bit" },
{ ".tex",	"text/plain: type=\"tex-or-latex-source\"",	"7bit" },
{ ".tcl",	"text/plain; type=\"tcl-source\"",		"7bit" },
{ ".score",	"text/plain; type=\"scorefile-music\"",		"7bit" },
{ ".s",		"text/plain; type=\"assembly-source\"",		"7bit" },
{ ".rtf",	"text/plain; type=\"microsoft-rtf\"",		"7bit" },
{ ".r",		"text/plain; type=\"ratfor-source\"",		"7bit" },
{ ".pswm",	"text/plain; type=\"pswrap-and-objective-c\"",	"7bit" },
{ ".psw",	"text/plain; type=\"pswrap-source\"",		"7bit" },
{ ".ps",	"application/PostScript",			"7bit" },
{ ".p",		"text/plain; type=\"pascal-source\"",		"7bit" },
{ ".nroff",	"text/plain; type=\"nroff-source\"",		"7bit" },
{ ".nr",	"text/plain; type=\"nroff-source\"",		"7bit" },
{ ".ms",	"text/plain; type=\"nroff-source\"",		"7bit" },
{ ".me",	"text/plain; type=\"nroff-source\"",		"7bit" },
{ ".mbox",	"text/plain; type=\"mailbox\"",			"7bit" },
{ ".man",	"text/plain; type=\"man-page\"",		"7bit" },
{ ".ma",	"text/plain; type=\"mathematica-notebook\"",	"7bit" },
{ ".m4",	"text/plain; type=\"m4-source\"",		"7bit" },
{ ".m",		"text/plain; type=\"objective-c-source\"",	"7bit" },
{ ".lst",	"text/plain; type=\"dsp-listing\"",		"7bit" },
{ ".h",		"text/plainl type=\"c-header\"",		"7bit" },
{ ".gif",	"image/gif",					"base64"},
{ ".f",		"text/plain; type=\"fortran-source\"",		"7bit" },
{ ".eps",	"application/PostScript",			"7bit" },
{ ".doc",	"text/plain",					"7bit" },
{ ".c",		"text/plain; type=\"c-source\"",		"7bit" },
{ ".awk",	"text/plain; type=\"awk-source\"",		"7bit" },
{ ".asm",	"text/plain; type=\"dsp-assembler-source\"",	"7bit" },
{ ".ascii",	"text/plain",					"7bit" },
{ ".F",		"text/plain; type=\"fortran-source\"",		"7bit" },
{ "",		"",						"" }
};

static char *TransferEncoding = NULL,
	    *ContentType = NULL;

/*ARGSUSED*/
characterize(filename,fp) char *filename; FILE *fp;{
	char	*p, *strrchr();
	struct exttable_t *q;

	if ((p= strrchr(filename,'.')) == NULL) {
		TransferEncoding = "base64";
		ContentType = "application/octet-stream";
	}
	else {
		for (q= &ExtTable[0]; *q->ext != NULL; q++) {
			if (strncmp(q->ext,p,strlen(q->ext))==0) {
				TransferEncoding = q->encoding;
				ContentType =  q->content_type;
				break;
			}	
		}
		if (*q->ext == NULL) {
			TransferEncoding = "base64";
			ContentType = "application/octet-stream";
		}
	}
}
 char *
contentType() {
	return ContentType;
}
 char *
transferEncoding() {
	return TransferEncoding;
}

/*
 * to64 -- a filter from ascii to base64 encoding, from the
 *      metamail distribution.
 */
/*
Copyright (c) 1991 Bell Communications Research, Inc. (Bellcore)

Permission to use, copy, modify, and distribute this material 
for any purpose and without fee is hereby granted, provided 
that the above copyright notice and this permission notice 
appear in all copies, and that the name of Bellcore not be 
used in advertising or publicity pertaining to this 
material without the specific, prior written permission 
of an authorized representative of Bellcore.  BELLCORE 
MAKES NO REPRESENTATIONS ABOUT THE ACCURACY OR SUITABILITY 
OF THIS MATERIAL FOR ANY PURPOSE.  IT IS PROVIDED "AS IS", 
WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
*/

static char basis_64[] =
   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

to64(infile, outfile) 
FILE *infile, *outfile;
{
    int c1, c2, c3, ct=0;
    while ((c1 = getc(infile)) != EOF) {
        c2 = getc(infile);
        if (c2 == EOF) {
            output64chunk(c1, 0, 0, 2, outfile);
        } else {
            c3 = getc(infile);
            if (c3 == EOF) {
                output64chunk(c1, c2, 0, 1, outfile);
            } else {
                output64chunk(c1, c2, c3, 0, outfile);
            }
        }
        ct += 4;
        if (ct > 71) {
            (void) putc('\n', outfile);
            ct = 0;
        }
    }
    if (ct) (void) putc('\n', outfile);
    (void) fflush(outfile);
}

output64chunk(c1, c2, c3, pads, outfile)
FILE *outfile;
{
    (void) putc(basis_64[c1>>2], outfile);
    (void) putc(basis_64[((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4)], outfile);
    if (pads == 2) {
        (void) putc('=', outfile);
        (void) putc('=', outfile);
    } else if (pads) {
        (void) putc(basis_64[((c2 & 0xF) << 2) | ((c3 & 0xC0) >>6)], outfile);
        (void) putc('=', outfile);
    } else {
        (void) putc(basis_64[((c2 & 0xF) << 2) | ((c3 & 0xC0) >>6)], outfile);
        (void) putc(basis_64[c3 & 0x3F], outfile);
    }
}
/* End of Bellcore material. */


/*
** Translation functions.
*/

/*
 * rtfShred -- turn index.rtf files (which are very stereotyped)
 *	into plain text, mostly by discarding things.
 */
rtfShred(ifp,ofp) FILE *ifp, *ofp; {
	int	ch;
	char	*p, buffer[MAXLINE];

	while ((ch = getc(ifp)) != EOF) {
		switch (ch) {
		case '{':
		case '}':
			break;
		case '\\':
			for (p= buffer; (ch= getc(ifp)) != EOF; p++) {
				if (isalnum(ch)) {
					*p = ch;
				}
				else {
					*p = ch; /* either this line... */
					p[1] = NULL;
					(void) ungetc(ch,ifp); /* or this is wrong. */
					break;
				}
			}
			if (strncmp(buffer,"attachment",10)==0) {
				(void) fprintf(ofp," (%s ",buffer);
				for (ch= getc(ifp); (ch= getc(ifp)) != EOF && ch != '}'; ) {
					if (isprint(ch))
						(void) putc(ch,ofp);
				}
				(void) fprintf(ofp," goes here) ");
			}
			if (strncmp(buffer,"fonttbl",7)==0) {
				/* Skip to closing } */
				while ((ch= getc(ifp)) != EOF && ch != '}')
					;
			}
			break;
		default:
			(void) putc(ch,ofp);
			break;
		}
	}
}



/*
** Supporting libraries -- signals
*/
/*
 * alarmHandler -- a ``normal'' non-portable version of an alarm handler.
 *	Alas, setting a flag and returning is not fully functional in
 *      BSD: system calls don't fail when reading from a ``slow'' device
 *      like a socket. So we longjump instead, which is erronious on
 *      a small number of machines and ill-defined in the language.
 */
alarmHandler() {
	extern jmp_buf TimeoutJmpBuf;
	(void) signal(SIGALRM,SIG_IGN); 
	longjmp(TimeoutJmpBuf,(int)1);
}

interruptHandler() {
	extern jmp_buf InterruptJmpBuf;
	(void) signal(SIGHUP,SIG_IGN); 
	(void) signal(SIGINT,SIG_IGN); 
	longjmp(InterruptJmpBuf,(int)1);
}


/*
** Reporting amd logging library functions --
*/

/*
 * die -- say something and exit with a non-zero return code.
 */
/*VARARGS*/
die(format,p1,p2,p3,p4,p5) char *format,*p1,*p2,*p3,*p4,*p5; {
	(void) fprintf(stderr,"%s: ",ProgName);
	(void) fprintf(stderr,format,p1,p2,p3,p4,p5);
	(void) fprintf(stderr," (halting).\n");
	removeTempDir();
	exit(1);
}

/*
** temp directory creation/deletion library functions
**
*/

/*
 * makeTempDir -- make a temporary directory, by default in /usr/tmp.
 */
makeTempDir() {
	char buffer[MAXLINE]; 

	TmpDir = tmpnam((char *)NULL);
	(void) sprintf(buffer,"mkdir %s",TmpDir);
	if (system(buffer) != 0) {
		die("unable to create a temp directory %s\n",TmpDir);
	} 
	else if (Verbose) {
		(void) fprintf(stderr,"Created temp directory %s\n",
			 TmpDir);
	}
}

/*
 * removeTempDir -- make the temp directory disappear.
 */
removeTempDir() {
	char	buffer[MAXLINE]; 

	if (!CleanUp)
		return;

	(void) sprintf(buffer,"rm -r %s",TmpDir);
	if (system(buffer) != 0) {
		CleanUp = NO; /* Die calls removeTempDir, see... */
		die("unable to remove temp directory %s\n",TmpDir);
	} 
	else if (Verbose) {
		(void) fprintf(stderr,"Removed temp directory %s\n",TmpDir);
	}
}

#ifndef lint
static char *rcsid = "$Header: /team/davecb/Tools/n2m/src/RCS/n2m.c,v 1.16 1992/09/22 20:09:42 davecb Exp $";
#endif


/*
 * $Log: n2m.c,v $
 * Revision 1.16  1992/09/22  20:09:42  davecb
 * adding a message or two
 *
 * Revision 1.15  1992/09/22  19:04:36  davecb
 * adding quoting for tspecials
 *
 * Revision 1.14  1992/09/10  13:19:36  davecb
 * adding a fix to the base64 code (from bellcore!)
 *
 * Revision 1.13  1992/09/01  18:08:26  davecb
 * adding the translatuon for rtf
 *
 * Revision 1.12  1992/08/31  13:45:59  davecb
 * revers order of extension lookup
 *
 * Revision 1.11  1992/08/28  11:59:23  davecb
 * A few more file extensions, from memory
 *
 * Revision 1.10  1992/08/28  11:54:49  davecb
 * linting
 *
 * Revision 1.9  1992/08/28  01:33:34  davecb
 * making sys/stat.h compile
 *
 * Revision 1.8  1992/08/28  01:17:05  davecb
 * Adding the extensio s from NextAnswers
 *
 * Revision 1.7  1992/08/27  19:15:58  davecb
 * typo
 *
 * Revision 1.6  1992/08/27  19:13:02  davecb
 * more extensions for the table
 *
 * Revision 1.5  1992/08/27  13:54:52  davecb
 * adding the termination --
 *
 * Revision 1.4  1992/08/27  13:09:23  davecb
 * adding some file types
 *
 * Revision 1.3  1992/08/27  02:30:00  davecb
 * Adding Derek Beatty's amendments
 *
 * Revision 1.2  1992/08/25  20:16:04  davecb
 * *** empty log message ***
 *
 * Revision 1.1  1992/08/24  18:16:52  davecb
 * Initial revision
 *
 */

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.