ftp.nice.ch/pub/next/connectivity/mail/bundles/EnhanceMail.2.2p1.NIHS.bs.gnutar.gz#/EnhanceMail-2.2p1/Source/Send.m

This is Send.m in view mode; [Download] [Up]

/* -*-C-*-
*******************************************************************************
*
* File:         Send.m
* RCS:          /usr/local/sources/CVS/EnhanceMail/Send.m,v 1.19 1998/07/12 10:49:28 tom Exp
* Description:  
* Author:       Carl Edman
* Created:      Fri Oct 13 11:48:05 1995
* Modified:     Sun Mar  9 15:42:36 1997 (Tom Hageman)
* Language:     C
* Package:      N/A
* Status:       Experimental (Do Not Distribute)
*
* (C) Copyright 1995, but otherwise this file is perfect freeware.
*
*******************************************************************************
*/

#import <ctype.h>
#import "EnhanceMail.h"
#import "Send.h"
#import "SimpleString.h"
#import "Preferences.h"
#import "XFace.h"
#import "regexp.h"

static HashTable *SignatureHash=nil;
static HashTable *QuotationHash=nil;
static HashTable *ReplyBoxHash=nil;
static HashTable *InReplyToHash=nil;


static BOOL match_firstname(const char *str, const char **start, int *len)
{
   static regexp *namerx = 0;
   int i;

   if (!(str && *str)) return NO;

   if (!namerx)
      namerx = regcomp("^([^ \t_.%@,!]*[^ \t_%@,!])[^,!]*$|.*[,!][ \t]*([^ \t_.%@,!]*[^ \t_%@,!])");

   if (!namerx) return NO;
   if (!regexec(namerx, str)) return NO;

   for (i = 1;  i <= 2;  i++)
   {
      if (namerx->endp[i] > namerx->startp[i])
      {
	 (*start) = namerx->startp[i];
	 (*len) = namerx->endp[i] - namerx->startp[i];
	 return YES;
      }
   }
   return NO;
}


/* RFC822 Date:
   ^( *(Mon|Tue|Wed|Thu|Fri|Sat|Sun) *, )? *([0-9][0-9]?) +(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) +([0-9][0-9][0-9]?[0-9]?) +([0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?) +(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|1[A-Z]|\+[0-9][0-9][0-9][0-9]|-[0-9][0-9][0-9][0-9])
   */
static BOOL _match_date(const char *str, const char **start, int *len, int sub)
{
   static regexp *daterx = 0;

   if (!(str && *str)) return NO;

   if (!daterx)
      daterx = regcomp("^(([ \t]*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)[ \t]*,[ \t])?[ \t]*([0-9][0-9]?)[ \t]+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ \t]+([0-9][0-9][0-9]?[0-9]?))[ \t]+([0-9]?[0-9]:[0-9][0-9](:[0-9][0-9])?)");

   if (!daterx) return NO;
   if (!regexec(daterx, str)) return NO;

   (*start) = daterx->startp[sub];
   (*len) = daterx->endp[sub] - daterx->startp[sub];
   return YES;
}

static inline BOOL match_date(const char *str, const char **start, int *len)
{
   return _match_date(str, start, len, 1);
}

static inline BOOL match_time(const char *str, const char **start, int *len)
{
   return _match_date(str, start, len, 7);
}

static void append_initials(id s, const char *name, int nameLength)
{
   const unsigned char *d = name, *e;

   for (e = d + nameLength;  d < e;  d++)
   {
      if (NXIsAlpha(*d))
      {
	 BOOL wasUpper = NXIsUpper(*d);

	 [s appendString:d length:1];
	 while ((++d < e) && NXIsAlpha(*d))
	 {
	    // Handle names like "McDonald" => "MD", instead of just "M".
	    BOOL isUpper = NXIsUpper(*d);

	    if (!wasUpper && isUpper) [s appendString:d length:1];
	    wasUpper = isUpper;
	 }
      }
      if (*d == '@') break;   // Consider only name part of email address.
   }
}


// Handle a single escape sequence. assumption: c points to escape char.
static const char *handle_escape(const char *c, const char *end, id s, id mes, regexp *rx)
{
   const char *d;
   char *m;
   int n;

   if (c >= end) return c;

   switch (*c++)
   {
    case '{':  /* %{Header} expands to value for header. */
      for(d=c;(c<end) && (*c!='}');c++);
      m=alloca(c-d+1);
      strncpy(m,d,c-d);
      m[c-d]='\0';
      c++;
      if ((d = [mes headerValueForKey:m]))
	 [s appendString:d];
      break;

    case '!': /* "%!ab": conditional where escape `b' is expanded
		 only when escape `a' expands to an empty string. */
      n = [s length];
      c = handle_escape(c, end, s, mes, rx);
      c = handle_escape(c, end, ([s length] > n) ? nil : s, mes, rx);
      break;

    case '(': // Grouping; useful in conditionals.
      while (c < end && *c != ')')
      {
	 if (*c == '%' && (c + 1) < end)
	 {
	    c = handle_escape(++c, end, s, mes, rx);
	 }
	 else
	 {
	    [s appendChar:*(c++)];
	 }
      }
      c++;
      break;

    case 'd':
      if (match_date([mes headerValueForKey:"Date"], &d, &n))
         [s appendString:d length:n];
      break;
      
    case 't':
      if (match_time([mes headerValueForKey:"Date"], &d, &n))
         [s appendString:d length:n];
      break;
      
    case 'f':
      d = [mes headerValueForKey:"From"];
      if (!EnhanceGetUsername(d, &d, &n)) n = strlen(d);
      [s appendString:d length:n];
      break;

    case 'F': // First name.
      d = [mes headerValueForKey:"From"];
      if (!EnhanceGetUsername(d, &d, &n)) n = strlen(d);
      m = alloca(n+1);
      d = strncpy(m, d, n);
      m[n] = '\0';
      match_firstname(d, &d, &n);
      [s appendString:d length:n];
      break;
      
    case 'i':  // Initials
      if (EnhanceGetUsername([mes headerValueForKey:"From"], &d, &n))
	 append_initials(s, d, n);
      else
	 [s appendChar:'-'];
      break;

    case 'n':
    case 'N':
      [s appendChar:'\n'];
      break;

    case 'T':
      [s appendChar:'\t'];
      break;

    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      for(n=*(c-1)-'0';isdigit(*c) && (c<end);c++) n=n*10+(*c-'0');
      if (n<NSUBEXP && rx && rx->startp[n] && rx->endp[n])
         [s appendString:rx->startp[n] length:rx->endp[n]-rx->startp[n]];
      break;

    default:
      [s appendChar:*(c-1)];
      break;
   }
   return c;
}

// Interpret all escapes in a string.
static id interpret_escapes(const char *c, id mes, regexp *rx)
{
   id s = [[SimpleString alloc] init];
   const char *end = c + strlen(c);
   
   while (c < end)
   {
      if (*c == '%' && (c + 1) < end)
      {
	 c = handle_escape(++c, end, s, mes, rx);
      }
      else
      {
	 [s appendChar:*(c++)];
      }
   }
   return s;
}

///#ifndef KANJI
static id quote_roman_text(const char *text,int length,id mes)
{
   id s=[[SimpleString alloc] init],prefix=nil;
   int linebeg=0,i;
   int maxlen=[MailMessage lineLength];
   const char *line,*end,*word,*tmp,*prebeg,*preend;
   regexp *rx=regcomp(EnhanceQuoteRegex);

   if (!rx) return s;
   
   prebeg=preend=line=text;
   end=line+length;
   
   while(line<end)
   {
      if (!regexec(rx,line)) for(i=0;i<NSUBEXP;i++) rx->startp[i]=rx->endp[i]=line;
      word=rx->endp[0];
      
      if ((prefix==nil) ||
          ((word-line)!=(preend-prebeg)) ||
          (strncmp(line,prebeg,preend-prebeg)!=0))
      {
         prebeg=line;
         preend=word;
         if (prefix!=nil) prefix=[prefix free];
         prefix=interpret_escapes(EnhanceQuotePrefix, mes, rx);
         if (linebeg!=[s length]) linebeg=[s appendChar:'\n'];
      }
      
      while((word<end) && ((*word==' ')||(*word=='\t'))) word++;

      for(line=word;(line<end)&&(*line!='\n')&&(*line!='\r');line++);

      if ((*word=='\n')||(*word=='\r'))
      {
         if (linebeg!=[s length]) [s appendChar:'\n'];
         [s appendSimpleString:prefix];
         linebeg=[s appendChar:'\n'];
      }
      else if ([s length]-linebeg+(line-word)+
               (([s length]==linebeg) ? [prefix length] : 1)<maxlen)
      {
         if ([s length]==linebeg)
            [s appendSimpleString:prefix];
         else
            [s appendChar:' '];
         [s appendString:word length:(line-word)];
         linebeg=[s appendChar:'\n'];
      }
      else while(word<line)
      {
         for(tmp=word;(tmp<line)&&!((*tmp==' ')||(*tmp=='\t'));tmp++);
         if (!isascii(*tmp)) tmp++;
	 
         if ([s length]-linebeg+(tmp-word)+1>=maxlen)
            linebeg=[s appendChar:'\n'];
         
         if ([s length]==linebeg)
            [s appendSimpleString:prefix];
         else
            [s appendChar:' '];
         for(;word<tmp;word++) [s appendChar:*word];
         for(;(word<line)&&((*word==' ')||(*word=='\t'));word++);
      }
      
      if (line<end) line++;
      if ((line<end)&&((*line=='\n')||(*line=='\r'))&&(*line!=*(line-1))) line++;
   }
   
   if (linebeg!=[s length]) [s appendChar:'\n'];
   if (rx) free(rx);
   if (prefix!=nil) [prefix free];
   return s;
}

static int remove_roman_cell_droppings(char *buf, int len)
{
   /* Evil Hack(tm). Removes the droppings left around in the ASCII stream
      where the Text contained an embedded Cell.  This blindly removes
      _all_ occurences of this character, even when legitimately used !
      (so use this only when specifically requested by the user.) */
   const char DROPPING = '¬';
   register char *s = buf, c, *d = buf + len, *end = d;

   while (s < end)
   {
      if (*s++ == DROPPING)
      {
	 d = s - 1;
	 while (s < end)
	 {
	    if ((c = *s++) != DROPPING) *d++ = c; 
	 }
	 break;
      }
   }
   return (int)(d - buf);
}

///#else /* KANJI */

#define iskanji(c) ((c) & 0x80)
#define BPMC 2  // byte per multibyte char
#define KINSOKU_ON

#ifndef KINSOKU_ON
#define iskinsoku(c) (0)
#else /* KINSOKU_ON */
static int iskinsoku(const char *firstbyte)
{
   int i;
   const char kinsoku_char[][BPMC+1]=
   {
         {0xa1,0xa2,'\0'}, // comma (1)
         {0xa1,0xa3,'\0'}, // small circle
         {0xa1,0xa4,'\0'}, // comma (2)
         {0xa1,0xa5,'\0'}, // period
         {0xa1,0xa9,'\0'}, // question mark
         {0xa1,0xaa,'\0'}, // exclamation mark
         {0xa1,0xca,'\0'}, // round blacket (left)
         {0xa1,0xcb,'\0'}, // round blacket (right)
         {0xa1,0xd6,'\0'}, // angular blacket (left)
         {0xa1,0xd7,'\0'}, // anguler blacket (right)
   };
   const int ntypes=sizeof(kinsoku_char)/(BPMC+1);

   if (*firstbyte=='.'||*firstbyte=='?'||*firstbyte=='!')
      return 1;

   for (i=0;i<ntypes;i++)
      if (strncmp(firstbyte,kinsoku_char[i],BPMC)==0)
         return 1;

   return 0;
}
#endif /* KINSOKU_ON */

static int iseow(const char *d)
{
   static char *oldd;
   static int topoftext=1,secondbyte=0;
   static int retval=0,oretval=0;

   if (d==0)
   {
      topoftext=1;
      secondbyte=0;
      retval=oretval=0;
      return retval;
   }

   if (!iskanji(*d))
   {
      if ((*d==' ')||(*d=='\t')) retval=1;
      else  retval=0;
   }
   else
   {
      if (secondbyte)
      {
         if (oldd==d)
            retval=oretval;
         else
	 {
            secondbyte=0;
            if (iskinsoku(d+1)) retval=0;
            else retval=1;
	 }
      }
      else
      {
         if (oldd==d)
            retval=oretval;
         else
            secondbyte=1,retval=0;

	 if (topoftext)
	    if (oldd!=d) topoftext=0;
      }
   }

   oldd=(char *)d;
   oretval=retval;
   return retval;
}

static int hasperiod(const char *lineend, int linelen)
{
   char *tmp=(char *)lineend;
   int i;
   const char mbperiod[][BPMC+1]=
   {			   // multi-byte characters of
         {0xa1,0xa3,'\0'}, // small circle
         {0xa1,0xa5,'\0'}, // period
         {0xa1,0xa9,'\0'}, // question mark
         {0xa1,0xaa,'\0'}, // exclamation mark
   };
   const int ntypes=sizeof(mbperiod)/(BPMC+1);

   while (lineend-tmp<linelen)
   {
      if (isspace(*tmp)&&!iskanji(*tmp)) tmp--;
      else
      {
         if (!iskanji(*tmp))
	 {
            if (*tmp=='.'||*tmp=='?'||*tmp=='!')
               return 1;
            else
               return 0;
	 }

         for (i=0;i<ntypes;i++)
            if (strncmp(tmp-1,mbperiod[i],BPMC)==0)
               return 1;
         return 0;
      }
   }
   return 0;
}

static id quote_kanji_text(const char *text,int length,id mes)
{
   id s=[[SimpleString alloc] init],prefix=nil;
   int linebeg=0,i,shortline=0;
   int maxlen=[MailMessage lineLength];
   const char *line,*end,*word,*tmp,*prebeg,*preend;
   regexp *rx=regcomp(EnhanceQuoteRegex);
   char *lasteow;

   if (!rx) return s;
   
   prebeg=preend=line=text;
   end=line+length;
   iseow(0); tmp=0;
   
   while(line<end)
   {
      if (!regexec(rx,line)) for(i=0;i<NSUBEXP;i++) rx->startp[i]=rx->endp[i]=line;
      word=rx->endp[0];
      
      if ((prefix==nil) ||
          ((word-line)!=(preend-prebeg)) ||
          (strncmp(line,prebeg,preend-prebeg)!=0))
      {
         prebeg=line;
         preend=word;
         if (prefix!=nil) prefix=[prefix free];
         prefix=interpret_escapes(EnhanceQuotePrefix, mes, rx);
         if (linebeg!=[s length]) linebeg=[s appendChar:'\n'];
      }
      
      while((word<end) && iseow(word)) word++;

      for(line=word;(line<end)&&(*line!='\n')&&(*line!='\r');line++);

      if ((*word=='\n')||(*word=='\r'))
      {
         if (linebeg!=[s length]) [s appendChar:'\n'];
         [s appendSimpleString:prefix];
         linebeg=[s appendChar:'\n'];
         iseow(0);
      }
      else if ([s length]-linebeg+(line-word)+
               (([s length]==linebeg) ? [prefix length] : 1)<maxlen)
      {
         if ([s length]==linebeg)
            [s appendSimpleString:prefix];
         else
	    if (!iskanji(*(word-[prefix length]))) [s appendChar:' '];
         [s appendString:word length:(line-word)];
         linebeg=[s appendChar:'\n'];
         iseow(0);
      }
      else
      {
	 if (line-word+[prefix length]>maxlen) shortline=0;
	 else shortline=1;

         while(word<line)
	 {
            lasteow=(char *)tmp-1;
            for(tmp=word;(tmp<line)&&!iseow(tmp);tmp++);

	    if ([s length]-linebeg+(tmp-word)+1>=maxlen)
	    {
	       if (tmp-word>maxlen)
	       {
		  if ([s length]==linebeg) [s appendSimpleString:prefix];
		  do
		  {
		     [s appendChar:*word];
		     word++;
		  } while([s length]-linebeg<maxlen);
	       }
	       linebeg=[s appendChar:'\n'];
	    }

	    if ([s length]==linebeg)
	       [s appendSimpleString:prefix];
	    else
	       if (!iskanji(*lasteow)||!iskanji(*word))
		  [s appendChar:' '];

	    for(;word<tmp;word++) [s appendChar:*word];
	    if (iskanji(*tmp)) [s appendChar:*tmp];
            for(;(word<line)&&iseow(word);word++);
	 }

	 if ((shortline||hasperiod(tmp,maxlen)) && ([s length]!=linebeg))
	    linebeg=[s appendChar:'\n'];
	 iseow(0);
      }

      if (line<end) line++;
      if ((line<end)&&((*line=='\n')||(*line=='\r'))&&(*line!=*(line-1))) line++;
   }
   
   if (linebeg!=[s length]) [s appendChar:'\n'];
   if (rx) free(rx);
   if (prefix!=nil) [prefix free];
   return s;
}

static int remove_kanji_cell_droppings(char *buf, int len)
{
   // No-op for now.
   return len;
}
///#endif /* KANJI */

static id quote_text(const char *text,int length,id mes)
{
   return EnhanceUseKanji() ? quote_kanji_text(text,length,mes) : quote_roman_text(text,length,mes);
}

static int remove_cell_droppings(char *buf, int len)
{
   return EnhanceUseKanji() ? remove_kanji_cell_droppings(buf, len) : remove_roman_cell_droppings(buf, len);
}

@implementation EnhanceSend

+ finishLoading:(struct mach_header *)header
{
   [self poseAs:[self superclass]];
   SignatureHash=[[HashTable alloc] initKeyDesc:"@" valueDesc:"i"];
   QuotationHash=[[HashTable alloc] initKeyDesc:"@" valueDesc:"i"];
   ReplyBoxHash=[[HashTable alloc] initKeyDesc:"@" valueDesc:"%"];
   InReplyToHash=[[HashTable alloc] initKeyDesc:"@" valueDesc:"%"];
   return self;
}

+ startUnloading
{
   SignatureHash=[SignatureHash free];
   QuotationHash=[QuotationHash free];
   ReplyBoxHash=[ReplyBoxHash free];
   InReplyToHash=[InReplyToHash free];
   return self;
}

// Access methods.
- (BOOL)signature { return (int)[SignatureHash valueForKey:self]; } 
- (void)setSignature:(BOOL)flag { [SignatureHash insertKey:self value:(void*)(int)flag]; }

- (BOOL)quotation { return (int)[QuotationHash valueForKey:self]; } 
- (void)setQuotation:(BOOL)flag { [QuotationHash insertKey:self value:(void*)(int)flag]; }

- (NXAtom)replyBox { return (NXAtom)[ReplyBoxHash valueForKey:self]; } 
- (void)setReplyBox:(const char *)val
{
   if (val)
      [ReplyBoxHash insertKey:self value:(void *)NXUniqueString(val)];
   else
      [ReplyBoxHash removeKey:self];
}

- (NXAtom)inReplyTo { return (NXAtom)[InReplyToHash valueForKey:self]; } 
- (void)setInReplyTo:(const char *)val
{
   if (val)
      [InReplyToHash insertKey:self value:(void *)NXUniqueString(val)];
   else
      [InReplyToHash removeKey:self];
}

// Support: perform selector while suppressing visual artefacts in text.

- performBatchingDisplay:(SEL)aSelector with:arg
{
   id ret;
   Window *w = [self window];
   BOOL isDisplayEnabled = [w isDisplayEnabled];

   if (isDisplayEnabled) [w disableDisplay];

   ret = [self perform:aSelector with:arg];

   if (isDisplayEnabled)
   {
      [w reenableDisplay];
      [w displayIfNeeded];
///   [w flushWindow];
   }
   return ret;
}

- performBatchingDisplay:(SEL)aSelector
{
   return [self performBatchingDisplay:aSelector with:nil];
}


// Semi-private support methods for reading signature.

enum { SIG_PLAIN, SIG_RTF, SIG_RTFD };

- (BOOL)_readSignatureFromDir:(const char *)dir type:(int)type
{
   static const char *const ext[] = { "", ".rtf", ".rtfd" };
   char path[MAXPATHLEN+1];

   sprintf(path, "%s/%s%s", dir, EnhanceSignatureFilename, ext[type]);
   switch (type)
   {
   case SIG_PLAIN:
      return ([text openFrom:path] != nil);
   case SIG_RTF:
      return ([text openRichFrom:path] != nil);
   case SIG_RTFD:
      return ([text openRTFDFrom:path] == NX_RTFDErrorNone);
   }
   return NO;
}

- (BOOL)_readSignatureFromDir:(const char *)dir
{
   int type;

   if (!dir) return NO;

   if (sendFormat==0)
   {
      // prefer plainest signature.
      for (type = SIG_PLAIN;  type <= SIG_RTFD;  type++)
      {
	 if ([self _readSignatureFromDir:dir type:type]) return YES;
      }
   }
   else
   {
      // prefer richest signature.
      for (type = SIG_RTFD;  type >= SIG_PLAIN;  type--)
      {
	 if ([self _readSignatureFromDir:dir type:type]) return YES;
      }
   }
   return NO;
}

- (void)_normalizeSelection
{
   [text setSelGray:NX_BLACK];
   if (sendFormat!=0) [text setSelFont:[Text getDefaultFont]];
}

- (void)_retoucheSignature
{
   int end = [text textLength];
   // Make sure .signature ends with newline.
   if (end > 0)
   {
      char c;
      [text getSubstring:&c start:end-1 length:1];

      if (c != '\n')
      {
	 [text setSel:end:end];
	 [text replaceSel:"\n"];
	 end += 1;
      }
      // Avoid bleeding of .signature text properties in following text.
      [text setSel:end-1:end];
      [self _normalizeSelection];
   }
   // Avoid bleeding of .signature text properties in preceding text.
   [text setSel:0:0];
   [self _normalizeSelection];

   [text replaceSel:EnhanceSignatureSeparator];
   [text setSel:0:0];
   [text replaceSel:"\n"];
}

- (void)doSignature
{
   if (!editing && EnhanceSignatureFilename && [self signature])
   {
      id oreader=[self getMailbox];
      id obox=oreader ? [oreader mailbox] : nil;
      const char *box=obox ? [obox dirname] : 0;
      const char *home=NXHomeDirectory();

      if ([self _readSignatureFromDir:box] ||
	  [self _readSignatureFromDir:home])
      {
	 [self _retoucheSignature];
      }

      if (sendFormat==0) [self makeAscii:self];
      [self updateSpecialDelivery];
      [self updateSize:self];
      [text update];

      // Kludge; really should try harder to restore field/cursor pos.
      [text setSel:0:0];
   }
}

- (void)doQuote
{
   if (!editing && [self quotation])
   {
      id oreader=[self getMailbox];
      id orig=oreader ? [oreader mailMessage] : nil;
      id otext=oreader ? [oreader text] : nil;
      id s;
      int start,len;
      NXSelPt beg,end;
      char *buf,*c;
      const int max_bpmc=(EnhanceUseKanji() ? 3 : 1); // maximum bytes per multibyte char

      if ((orig==nil) || (otext==nil)) return;

      if (!editing && !(EnhanceSignatureFilename && [self signature]))
      {
	 // start from clean slate, but only if signature is not present.
	 [text setText:""];
      }
      s=[[SimpleString alloc] init];

      if (EnhanceQuoteIntro)
      {
	 [s appendAndFreeSimpleString:interpret_escapes(EnhanceQuoteIntro,orig,0)];
	 [s appendChar:'\n'];
      }

      [otext getSel:&beg:&end];
      if (beg.cp!=end.cp)
      {
	 start=beg.cp;
	 len=end.cp-beg.cp;
	 c=buf=NXZoneMalloc(EnhanceScratchZone(), len*max_bpmc+1);
	 len=[otext getSubstring:buf start:start length:len];
      }
      else
      {
	 start=0;
	 len=[otext byteLength];
	 buf=NXZoneMalloc(EnhanceScratchZone(),len+1);
	 [otext getSubstring:buf start:start length:len];
	 // Skip header.
	 for(c=buf;c<buf+len-1;c++) if ((c[0]=='\n') && (c[1]=='\n')) break;
	 while ((c<buf+len) && (*c=='\n')) c++;
	 len-=(c-buf);
      }

      if (EnhanceRemoveCellDroppings) len = remove_cell_droppings(c, len);

      start=[s textLength];
      [s appendAndFreeSimpleString:quote_text(c,len,orig)];
      len=[s textLength]-start;
      [s appendChar:'\n'];	// Make room for reply.

      [text setSel:0:0];
      [text replaceSel:[s string]];

      if ((sendFormat!=0) && EnhanceQuoteColoring)
      {
	 [text setSel:start:start+len];
	 [text setSelColor:EnhanceQuoteColor];
      }
      if (sendFormat==0) [self makeAscii:self];
      [self updateSpecialDelivery];
      [self updateSize:self];
      [text update];
      
      [text setSel:start+len+1:start+len+1]; // move cursor to beyond quoted text.
      [text scrollSelToVisible];

      free(buf);
      [s free];
   }
}

- (void)doQuoteSig
{
   if (!editing)
   {
      [self doSignature];
      [self doQuote];
   }
}

- initSendWindow:fp16
{
   id ret=[super initSendWindow:fp16];

   [self setSignature:(EnhanceInsertSignature^EnhanceControlP())];
   [self setQuotation:NO];
   [self setReplyBox:NULL];
   [self setInReplyTo:NULL];

   // Kludge to make size field work with display batching.
   if (sizeField) [sizeField setBackgroundGray:NX_LTGRAY];

   [self performBatchingDisplay:@selector(doSignature)];
   [form selectTextAt:0];

   return ret;
}

- free
{
   [SignatureHash removeKey:self];
   [QuotationHash removeKey:self];
   [ReplyBoxHash removeKey:self];
   [InReplyToHash removeKey:self];
   
   return [super free];
}

- deliver:button
{
   id ret, def;
   NXAtom replybox, origbox;

   // XXX modifiers overloaded with regular archiving controls.
   // XXX allow use of both archive (alt) and unarchive (ctrl) until we
   //  can come up with a better solution.
   if (EnhanceAutoSpellCheck &~ (EnhanceAlternateP() | EnhanceControlP()))
   {
      id spell=[NXSpellChecker sharedInstance];
      id panel=(spell!=nil) ? [spell spellingPanel] : nil;

      if ((spell!=nil)
          &&[spell checkSpelling:NX_CheckSpellingFromStart of:self])
      {
         if (panel!=nil) [panel orderFront:self];
         return nil;
      }
      else
      {
         if (panel!=nil) [panel orderOut:self];
      }
   }
   
   def=[Defaults new];
   origbox=NXUniqueString([def outgoingMailbox]);
   replybox=[self replyBox];
      
   if (EnhanceRepliesWithOriginals && replybox)
      [def setOutgoingMailbox:replybox];
   
   ret=[super deliver:button];

   if (EnhanceRepliesWithOriginals && replybox)
      [def setOutgoingMailbox:origbox];
   
   return ret;
}

- mailMessage
{
   id mes=[super mailMessage];
   id ximage;
   char buf[1024];
   const char *tmp;

   sprintf(buf, "%s %s (Enhance %s)", [NXApp appName], [NXApp appVersion], EnhanceVersion);
   [mes setHeaderKey:"X-Nextstep-Mailer" value:buf];

   if (tmp=[self inReplyTo])
   {
      [mes setHeaderKey:"In-Reply-To" value:tmp];
   }
   if (EnhanceInsertXFace && EnhanceXFace && *EnhanceXFace
       && (ximage=[[NXImage alloc] initFromFile:EnhanceXFace]))
   {
      char *tmp=[ximage xFace];
      ximage=[ximage free];
      if (tmp)
      {
         [mes setHeaderKey:"X-Face" value:tmp];
         free(tmp);
      }
   }
   if (EnhanceInsertXImageURL && EnhanceXImageURL && *EnhanceXImageURL &&
       EnhanceXImageURL[strcspn(EnhanceXImageURL, " \n\t\r")] == '\0')
   {
      [mes setHeaderKey:"X-Image-URL" value:EnhanceXImageURL];
   }
   return mes;
}

- (void)_enhanceDefaultsChanged
{
   [super defaultsChanged];
   [self doQuoteSig]; // Sig defaults may have changed.
}

- (void)defaultsChanged
{
   [self performBatchingDisplay:@selector(_enhanceDefaultsChanged)];
}

- (void)_enhanceSetSendFormat:(int)arg
{
   [super setSendFormat:arg];
   [self doQuoteSig]; // Sig is dependent on message format.
}

- (void)setSendFormat:(int)arg
{
   [self performBatchingDisplay:@selector(_enhanceSetSendFormat:) with:(id)arg];
}

- restoreDraftFromMessage:message
{
#if 0
   if (EnhanceRestoreDeletesDraft)
   {
      [toc setState:'d' forMsg:number flush:YES];
   }
#endif
   // Suppress signature quoting.
   [self setSignature:NO];
   [text setText:""];
   [self updateSpecialDelivery];
   return [super restoreDraftFromMessage:message];
}

- _enhanceReplyHelper:(BOOL)flag
{
   id oreader=[self getMailbox];
   id obox=oreader ? [oreader mailbox] : nil;
   id orig=oreader ? [oreader mailMessage] : nil;
   int wasEditing = editing;
   id ret=[super replyHelper:flag];

   // 3.3 replyHelper leaves editing flag alone, but 4.x changes it.
   // For quoting/signature to work, editing must be off the first time.
   if (editing && !wasEditing)
   {
      editing = NO;
      [[self window] setDocEdited:NO];
   }

   if ((orig != nil) && EnhanceFlagReplies) [oreader setFlagged:YES];

   [self setReplyBox:[obox dirname]];
   [self setInReplyTo:[orig headerValueForKey:"Message-Id"]];

   if (!editing)
   {
      BOOL wasQuoting = [self quotation];
      BOOL willQuote = ((wasQuoting | EnhanceQuoteReplies) ^ EnhanceControlP());

      if (wasQuoting != willQuote)
      {
	 [self setQuotation:willQuote];
	 [self doQuoteSig];
      }
   }
   return ret;
}

- replyHelper:(BOOL)flag
{
   return [self performBatchingDisplay:@selector(_enhanceReplyHelper:) with:(id)(int)flag];
}


// <NXReadonlyTextStream> methods. (NXSpellChecker delegate)
- openTextStream
{
   [text openTextStream];
   return self;
}

- (BOOL)seekToCharacterAt:(int)offset relativeTo:(int)seekMode
{
   return [text seekToCharacterAt:offset relativeTo:seekMode];
}

- (int)readCharacters:(char *)buffer count:(int)count
{
   return [text readCharacters:buffer count:count];
}

- (int)currentCharacterOffset
{
   return [text currentCharacterOffset];
}

- (BOOL)isAtEOTS
{
   return [text isAtEOTS];
}

- closeTextStream
{
   [text closeTextStream];
   return self;
}

// <NXSelectText> methods. (NXSpellChecker delegate)
- (void)selectCharactersFrom:(int)start to:(int)end
{
   [text selectCharactersFrom:start to:end];
}

- (int)selectionCharacterCount
{
   return [text selectionCharacterCount];
}

- (int)readCharactersFromSelection:(char *)buffer count:(int)count
{
   return [text readCharactersFromSelection:buffer count:count];
}

- (void)makeSelectionVisible
{
   [text makeSelectionVisible];
}

// <NXChangeSpelling> methods.
- changeSpelling:sender
{
   return ([text changeSpelling:sender]==nil) ? nil : self;
}

// <NXIgnoreMisspelledWords> methods.
- (int)spellDocumentTag
{
   return [text spellDocumentTag];
}

@end // EnhanceSend


@implementation MediaText(EnhanceSend)

/* Support functions: open a stream for reading a named file.
   This used to try NXMapFile first, then if that doesn't work try NXOpenFile.
   But since that exercise was meant to allow reading from a fifo, and
   NXMapFile silently succeeds opening the fifo (just doesn't return any
   data) just use NXOpenFile. -- it's purportedly less efficient but for now
   we're only using it to read signature files, so efficiency is not really
   important :-)
   (we have to pass a file descriptor back to the caller, since NXStream
   has no (public) way of determining how it was opened,
   and closing it is dependent on what type it was -- bad abstraction:-|) */

static NXStream *openStreamForReading(const char *file, int *fdP)
{
   int fd = -1;
   NXStream *s = NULL;
#if 0
   NXStream *s = NXMapFile(file, NX_READONLY);

   if (s == NULL && errno != ENOENT && fdP)
#endif
   {
      if ((fd = open(file, O_RDONLY)) >= 0)
      {
	 if ((s = NXOpenFile(fd, NX_READONLY)) == NULL)
	 {
	    close(fd);
	    fd = -1;
	 }
      }
   }
   if (fdP) *fdP = fd;
   return s;
}

static void closeStream(NXStream *s, int fd)
{
   if (s)
   {
      if (fd >= 0)
      {
	 NXClose(s);
      }
      else
      {
	 NXCloseMemory(s, NX_FREEBUFFER);
      }
   }
   if (fd >= 0) close(fd);
}


- openFrom:(const char *)file
{
   id ret = nil;
   int fd;
   NXStream *s = openStreamForReading(file, &fd);
   
   if (s)
   {
      ret = [self readText:s];
      closeStream(s, fd);
   }
   return ret;
}

- openRichFrom:(const char *)file
{
   id ret = nil;
   int fd;
   NXStream *s = openStreamForReading(file, &fd);
   
   if (s)
   {
      ret = [self readRichText:s];
      closeStream(s, fd);
   }
   return ret;
}

@end // MediaText (EnhanceSend)


// Overrule SendDemo to kludge around rich signature weirdness.

@interface EnhanceSendDemo: SendDemo
- (int)openSend:(int *)aWindow;
@end


@implementation EnhanceSendDemo

+ finishLoading:(struct mach_header *)header
{
   [self poseAs:[self superclass]];
   [[SendDemo new] changeClassTo:[self class]];
   return self;
}

- (int)openSend:(int *)aWindow
{
   int ret;
   int oInsertSignature = EnhanceInsertSignature;

   EnhanceInsertSignature = NO;
   ret = [super openSend:aWindow];
   EnhanceInsertSignature = oInsertSignature;

   return ret;
}

@end // EnhanceSendDemo

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