1/************************************************************************
2 *	Routines that deal with the mailfolder(format)			*
3 *									*
4 *	Copyright (c) 1990-1999, S.R. van den Berg, The Netherlands	*
5 *	Copyright (c) 1999-2001, Philip Guenther, The United States	*
6 *							of America	*
7 *	#include "../README"						*
8 ************************************************************************/
9#ifdef RCS
10static /*const*/char rcsid[]=
11 "$Id: mailfold.c,v 1.104 2001/08/04 07:16:26 guenther Exp $";
12#endif
13#include "procmail.h"
14#include "acommon.h"
15#include "sublib.h"
16#include "robust.h"
17#include "misc.h"
18#include "memblk.h"
19#include "pipes.h"
20#include "common.h"
21#include "exopen.h"
22#include "goodies.h"
23#include "variables.h"
24#include "locking.h"
25#include "lastdirsep.h"
26#include "foldinfo.h"
27#include "from.h"
28#include "shell.h"
29#include "mailfold.h"
30
31int logopened,rawnonl;
32off_t lasttell;
33static long lastdump;
34static volatile int mailread;	/* if the mail is completely read in already */
35static struct dyna_array confield;		  /* escapes, concatenations */
36static const char*realstart,*restbody;
37static const char from_expr[]=FROM_EXPR;
38
39static const char*fifrom(fromw,lbound,ubound)
40 const char*fromw,*const lbound;char*const ubound;
41{ int i;					   /* terminate & scan block */
42  i= *ubound;*ubound='\0';fromw=strstr(mx(fromw,lbound),from_expr);*ubound=i;
43  return fromw;
44}
45
46static int doesc;
47			       /* inserts escape characters on outgoing mail */
48static long getchunk(s,fromw,len)const int s;const char*fromw;const long len;
49{ static const char esc[]=ESCAP,*ffrom,*endp;
50  if(doesc)		       /* still something to escape since last time? */
51     doesc=0,rwrite(s,esc,STRLEN(esc)),lastdump++;		/* escape it */
52  ffrom=0;					 /* start with a clean slate */
53  if(fromw<thebody)			   /* are we writing the header now? */
54     ffrom=fifrom(fromw,realstart,thebody);		      /* scan header */
55  if(!ffrom&&(endp=fromw+len)>restbody)	       /* nothing yet? but in range? */
56   { if((endp+=STRLEN(from_expr)-1)>(ffrom=themail.p+filled))	/* add slack */
57	endp=(char*)ffrom;		  /* make sure we stay within bounds */
58     ffrom=fifrom(fromw,restbody,endp);			  /* scan body block */
59   }
60  return ffrom?(doesc=1,(ffrom-fromw)+1L):len;	 /* +1 to write out the '\n' */
61}
62
63#ifdef sMAILBOX_SEPARATOR
64#define smboxseparator(fd)	(ft_delim(type)&&\
65 (part=len,rwrite(fd,sMAILBOX_SEPARATOR,STRLEN(sMAILBOX_SEPARATOR))))
66#define MAILBOX_SEPARATOR
67#else
68#define smboxseparator(fd)
69#endif /* sMAILBOX_SEPARATOR */
70#ifdef eMAILBOX_SEPARATOR
71#define emboxseparator(fd)	\
72 (ft_delim(type)&&rwrite(fd,eMAILBOX_SEPARATOR,STRLEN(eMAILBOX_SEPARATOR)))
73#ifndef MAILBOX_SEPARATOR
74#define MAILBOX_SEPARATOR
75#endif
76#else
77#define emboxseparator(fd)
78#endif /* eMAILBOX_SEPARATOR */
79
80long dump(s,type,source,len)const int s,type;const char*source;
81 long len;
82{ int i;long part;
83  lasttell=i= -1;SETerrno(EBADF);
84  if(s>=0)
85   { if(ft_lock(type)&&(lseek(s,(off_t)0,SEEK_END),fdlock(s)))
86	nlog("Kernel-lock failed\n");
87     lastdump=len;doesc=0;
88     if(ft_delim(type)&&!rawnonl)
89	part=getchunk(s,source,len);			/* must escape From_ */
90     else
91	part=len;
92     lasttell=lseek(s,(off_t)0,SEEK_END);
93     if(!rawnonl)
94      { smboxseparator(s);			       /* optional separator */
95#ifndef NO_NFS_ATIME_HACK	       /* if it is a file, trick NFS into an */
96	if(part&&ft_atime(type))			    /* a_time<m_time */
97	 { struct stat stbuf;
98	   rwrite(s,source++,1);len--;part--;		     /* set the trap */
99	   if(fstat(s,&stbuf)||					  /* needed? */
100	    stbuf.st_mtime==stbuf.st_atime)
101	      ssleep(1);  /* ...what a difference this (tea) second makes... */
102	 }
103#endif
104      }
105     goto jin;
106     do
107      { part=getchunk(s,source,len);
108jin:	while(part&&(i=rwrite(s,source,BLKSIZ<part?BLKSIZ:(int)part)))
109	 { if(i<0)
110	      goto writefin;
111	   part-=i;len-=i;source+=i;
112	 }
113      }
114     while(len);
115     if(!rawnonl)
116      { if(!len&&(lastdump<2||!(source[-1]=='\n'&&source[-2]=='\n'))&&
117	 ft_forceblank(type))
118	   lastdump++,rwrite(s,newline,1);     /* message always ends with a */
119	emboxseparator(s);	 /* newline and an optional custom separator */
120      }
121writefin:
122     i=type!=ft_PIPE&&fsync(s)&&errno!=EINVAL;	  /* EINVAL => wasn't a file */
123     if(ft_lock(type))
124      { int serrno=errno;		       /* save any error information */
125	if(fdunlock())
126	   nlog("Kernel-unlock failed\n");
127	SETerrno(serrno);
128      }
129     i=rclose(s)||i;
130   }			   /* return an error even if nothing was to be sent */
131  return i&&!len?-1:len;
132}
133
134static int dirfile(chp,linkonly,type)char*const chp;const int linkonly,type;
135{ static const char lkingto[]="Linking to";struct stat stbuf;
136  if(type==ft_MH)
137   { long i=0;			     /* first let us try to prime i with the */
138#ifndef NOopendir		     /* highest MH folder number we can find */
139     long j;DIR*dirp;struct dirent*dp;char*chp2;
140     if(dirp=opendir(buf))
141      { while(dp=readdir(dirp))		/* there still are directory entries */
142	   if((j=strtol(dp->d_name,&chp2,10))>i&&!*chp2)
143	      i=j;			    /* yep, we found a higher number */
144	closedir(dirp);				     /* aren't we neat today */
145      }
146     else
147	readerr(buf);
148#endif /* NOopendir */
149     if(chp-buf+sizeNUM(i)>linebuf)
150exlb: { nlog(exceededlb);setoverflow();
151	goto ret;
152      }
153     ;{ int ok;
154	do ultstr(0,++i,chp);		       /* find first empty MH folder */
155	while((ok=linkonly?rlink(buf2,buf,0):hlink(buf2,buf))&&errno==EEXIST);
156	if(linkonly)
157	 { yell(lkingto,buf);
158	   if(ok)
159	      goto nolnk;
160	   goto didlnk;
161	 }
162      }
163     goto opn;
164   }
165  else if(type==ft_MAILDIR)
166   { if(!unique(buf,chp,linebuf,NORMperm,verbose,doMAILDIR))
167	goto ret;
168     unlink(buf);			 /* found a name, remove file in tmp */
169     memcpy(chp-MAILDIRLEN-1,maildirnew,MAILDIRLEN);	/* but link directly */
170   }								 /* into new */
171  else								   /* ft_DIR */
172   { size_t mpl=strlen(msgprefix);
173     if(chp-buf+mpl+sizeNUM(stbuf.st_ino)>linebuf)
174	goto exlb;
175     stat(buf2,&stbuf);			      /* filename with i-node number */
176     ultoan((unsigned long)stbuf.st_ino,strcpy(chp,msgprefix)+mpl);
177   }
178  if(linkonly)
179   { yell(lkingto,buf);
180     if(rlink(buf2,buf,0)) /* hardlink the new file, it's a directory folder */
181nolnk:	nlog("Couldn't make link to"),logqnl(buf);
182     else
183didlnk: appendlastvar(buf);		     /* lastvar is "LASTFOLDER" here */
184     goto ret;
185   }
186  if(!rlink(buf2,buf,0))			      /* try rename-via-link */
187opn: unlink(buf2);			     /* success; remove the original */
188  else if(errno=EEXIST||!stat(buf,&stbuf)||errno!=ENOENT||rename(buf2,buf))
189ret: return -1;	 /* rename it, but only if it won't replace an existing file */
190  setlastfolder(buf);
191  return opena(buf);
192}
193
194int writefolder(boxname,linkfolder,source,len,ignwerr,dolock)
195 char*boxname,*linkfolder;const char*source;long len;const int ignwerr,dolock;
196{ char*chp,*chp2;mode_t mode;int fd,type;
197  if(*boxname=='|'&&(!linkfolder||linkfolder==Tmnate))
198   { setlastfolder(boxname);
199     fd=rdup(Deliverymode==2?STDOUT:savstdout);
200     type=ft_PIPE;
201     goto dumpc;
202   }
203  if(boxname!=buf)
204     strcpy(buf,boxname);		 /* boxname can be found back in buf */
205  if(linkfolder)		    /* any additional directories specified? */
206   { size_t blen;
207     if(blen=Tmnate-linkfolder)		       /* copy the names into safety */
208	Tmnate=(linkfolder=tmemmove(malloc(blen),linkfolder,blen))+blen;
209     else
210	linkfolder=0;
211   }
212  type=foldertype(0,0,&mode,0);			     /* the envelope please! */
213  chp=strchr(buf,'\0');
214  switch(type)
215   { case ft_FILE:
216	if(linkfolder)	  /* any leftovers?  Now is the time to display them */
217	   concatenate(linkfolder),skipped(linkfolder),free(linkfolder);
218	if(!strcmp(devnull,buf))
219	   type=ft_PIPE,rawnonl=1;	     /* save the effort on /dev/null */
220	else if(!(UPDATE_MASK&(mode|cumask)))
221	   chmod(boxname,mode|UPDATE_MASK);
222	if(dolock&&type!=ft_PIPE)
223	 { strcpy(chp,lockext);
224	   if(!globlock||strcmp(buf,globlock))
225	      lockit(tstrdup(buf),&loclock);
226	   *chp='\0';
227	 }
228	setlastfolder(boxname);
229	fd=opena(boxname);
230dumpc:	if(dump(fd,type,source,len)&&!ignwerr)
231dumpf:	 { switch(errno)
232	    { case ENOSPC:nlog("No space left to finish writing"),logqnl(buf);
233		 break;
234#ifdef EDQUOT
235	      case EDQUOT:nlog("Quota exceeded while writing"),logqnl(buf);
236		 break;
237#endif
238	      default:writeerr(buf);
239	    }
240	   if(lasttell>=0&&!truncate(boxname,lasttell)&&(logopened||verbose))
241	      nlog("Truncated file to former size\n");	    /* undo garbage */
242ret0:	   return 0;
243	 }
244	return 1;
245     case ft_TOOLONG:
246exlb:	nlog(exceededlb);setoverflow();
247     case ft_CANTCREATE:
248retf:	if(linkfolder)
249	   free(linkfolder);
250	goto ret0;
251     case ft_MAILDIR:
252	if(source==themail.p)			      /* skip leading From_? */
253	   source=skipFrom_(source,&len);
254	strcpy(buf2,buf);
255	chp2=buf2+(chp-buf)-MAILDIRLEN;
256	*chp++= *MCDIRSEP_;
257	;{ int retries=MAILDIRretries;
258	   for(;;)
259	    { struct stat stbuf;
260	      if(0>(fd=unique(buf,chp,linebuf,NORMperm,verbose,doFD|doMAILDIR)))
261		 goto nfail;
262	      if(dump(fd,ft_MAILDIR,source,len)&&!ignwerr)
263		 goto failed;
264	      strcpy(chp2,maildirnew);
265	      chp2+=MAILDIRLEN;
266	      *chp2++= *MCDIRSEP_;
267	      strcpy(chp2,chp);
268	      if(!rlink(buf,buf2,0))
269	       { unlink(buf);
270		 break;
271	       }
272	      else if(errno!=EEXIST&&lstat(buf2,&stbuf)&&errno==ENOENT&&
273	       !rename(buf,buf2))
274		 break;
275	      unlink(buf);
276	      if(!retries--)
277		 goto nfail;
278	    }
279	 }
280	setlastfolder(buf2);
281	break;
282     case ft_MH:
283#if 0
284	if(source==themail.p)
285	   source=skipFrom_(source,&len);
286#endif
287     default:						     /* case ft_DIR: */
288	*chp++= *MCDIRSEP_;
289	strcpy(buf2,buf);
290	chp2=buf2+(chp-buf);
291	if(!unique(buf2,chp2,linebuf,NORMperm,verbose,0)||
292	 0>(fd=dirfile(chp,0,type)))
293nfail:	 { nlog("Couldn't create or rename temp file");logqnl(buf);
294	   goto retf;
295	 }
296	if(dump(fd,type,source,len)&&!ignwerr)
297	 { strcpy(buf,buf2);
298failed:	   unlink(buf);lasttell= -1;
299	   if(linkfolder)
300	      free(linkfolder);
301	   goto dumpf;
302	 }
303	strcpy(buf2,buf);
304	break;
305   }
306  if(!(UPDATE_MASK&(mode|cumask)))
307   { chp[-1]='\0';				      /* restore folder name */
308     chmod(buf,mode|UPDATE_MASK);
309   }
310  if(linkfolder)				 /* handle secondary folders */
311   { for(boxname=linkfolder;boxname!=Tmnate;boxname=strchr(boxname,'\0')+1)
312      { strcpy(buf,boxname);
313	switch(type=foldertype(0,1,&mode,0))
314	 { case ft_TOOLONG:goto exlb;
315	   case ft_CANTCREATE:continue;			     /* just skip it */
316	   case ft_DIR:case ft_MH:case ft_MAILDIR:
317	      chp=strchr(buf,'\0');
318	      *chp= *MCDIRSEP_;
319	      if(dirfile(chp+1,1,type)) /* link it with the original in buf2 */
320		 if(!(UPDATE_MASK&(mode|cumask)))
321		  { *chp='\0';
322		    chmod(buf,mode|UPDATE_MASK);
323		  }
324	      break;
325	 }
326      }
327     free(linkfolder);
328   }
329  return 1;
330}
331
332void logabstract(lstfolder)const char*const lstfolder;
333{ if(lgabstract>0||(logopened||verbose)&&lgabstract)  /* don't mail it back? */
334   { char*chp,*chp2;int i;static const char sfolder[]=FOLDER;
335     if(mailread)			  /* is the mail completely read in? */
336      { i= *thebody;*thebody='\0';     /* terminate the header, just in case */
337	if(eqFrom_(chp=themail.p))		       /* any "From " header */
338	 { if(chp=strchr(themail.p,'\n'))
339	      *chp='\0';
340	   else
341	      chp=thebody;			  /* preserve mailbox format */
342	   elog(themail.p);elog(newline);*chp='\n';	     /* (any length) */
343	 }
344	*thebody=i;			   /* eliminate the terminator again */
345	if(!nextexit&&				/* don't reenter malloc/free */
346	 (chp=egrepin(NSUBJECT,chp,(long)(thebody-chp),0)))
347	 { size_t subjlen;
348	   for(chp2= --chp;*--chp2!='\n';);
349	   if((subjlen=chp-++chp2)>MAXSUBJECTSHOW)
350	      subjlen=MAXSUBJECTSHOW;		    /* keep it within bounds */
351	   ((char*)tmemmove(buf,chp2,subjlen))[subjlen]='\0';detab(buf);
352	   elog(" ");elog(buf);elog(newline);
353	 }
354      }
355     elog(sfolder);strlcpy(buf,lstfolder,MAXfoldlen);detab(buf);elog(buf);
356     i=strlen(buf)+STRLEN(sfolder);i-=i%TABWIDTH;		/* last dump */
357     do elog(TABCHAR);
358     while((i+=TABWIDTH)<LENoffset);
359     ultstr(7,lastdump,buf);elog(buf);elog(newline);
360   }
361}
362
363static int concnd;				 /* last concatenation value */
364
365void concon(ch)const int ch;   /* flip between concatenated and split fields */
366{ size_t i;
367  if(concnd!=ch)				   /* is this run redundant? */
368   { concnd=ch;			      /* no, but note this one for next time */
369     for(i=confield.filled;i;)		   /* step through the saved offsets */
370	themail.p[acc_vall(confield,--i)]=ch;	       /* and flip every one */
371   }
372}
373
374void readmail(rhead,tobesent)const long tobesent;
375{ char*chp,*pastend;static size_t contlengthoffset;
376  ;{ long dfilled;
377     if(rhead==2)		  /* already read, just examine what we have */
378	dfilled=mailread=0;
379     else if(rhead)				/* only read in a new header */
380      { memblk new;
381	dfilled=mailread=0;makeblock(&new,0);readdyn(&new,&dfilled,0);
382	if(tobesent>dfilled&&isprivate)		     /* put it in place here */
383	 { tmemmove(themail.p+dfilled,thebody,filled-=tobesent);
384	   tmemmove(themail.p,new.p,dfilled);
385	   resizeblock(&themail,filled+=dfilled,1);
386	   freeblock(&new);
387	 }
388	else			   /* too big or must share -- switch blocks */
389	 { resizeblock(&new,filled-tobesent+dfilled,0);
390	   tmemmove(new.p+dfilled,thebody,filled-=tobesent);
391	   freeblock(&themail);
392	   themail=new;private(1);
393	   filled+=dfilled;
394	 }
395      }
396     else
397      { if(!mailread||!filled)
398	   rhead=1;	 /* yup, we read in a new header as well as new mail */
399	mailread=0;dfilled=thebody-themail.p;
400	if(!isprivate)
401	 { memblk new;
402	   makeblock(&new,filled);
403	   if(filled)
404	      tmemmove(new.p,themail.p,filled);
405	   freeblock(&themail);
406	   themail=new;private(1);
407	 }
408	readdyn(&themail,&filled,filled+tobesent);
409      }
410     pastend=filled+(thebody=themail.p);
411     while(thebody<pastend&&*thebody++=='\n');	     /* skip leading garbage */
412     realstart=thebody;
413     if(rhead)			      /* did we read in a new header anyway? */
414      { confield.filled=0;concnd='\n';
415	while(thebody=strchr(thebody,'\n'))
416	   switch(*++thebody)			    /* mark continued fields */
417	    { case '\t':case ' ':app_vall(confield,(long)(thebody-1-themail.p));
418	      default:
419		 continue;		   /* empty line marks end of header */
420	      case '\n':thebody++;
421		 goto eofheader;
422	    }
423	thebody=pastend;      /* provide a default, in case there is no body */
424eofheader:
425	contlengthoffset=0;		      /* traditional Berkeley format */
426	if(!berkeley&&				  /* ignores Content-Length: */
427	   (chp=egrepin("^Content-Length:",themail.p,
428			(long)(thebody-themail.p),0)))
429	   contlengthoffset=chp-themail.p;
430      }
431     else			       /* no new header read, keep it simple */
432	thebody=themail.p+dfilled; /* that means we know where the body starts */
433   }		      /* to make sure that the first From_ line is uninjured */
434  if((chp=thebody)>themail.p)
435     chp--;
436  if(contlengthoffset)
437   { unsigned places;long cntlen,actcntlen;charNUM(num,cntlen);
438     chp=themail.p+contlengthoffset;cntlen=filled-(thebody-themail.p);
439     if(filled>1&&themail.p[filled-2]=='\n')		 /* no phantom '\n'? */
440	cntlen--;		     /* make sure it points to the last '\n' */
441     for(actcntlen=places=0;;)
442      { switch(*chp)
443	 { default:					/* fill'r up, please */
444	      if(places<=sizeof num-2)
445		 *chp++='9',places++,actcntlen=(unsigned long)actcntlen*10+9;
446	      else
447		 *chp++=' ';		 /* ultra long Content-Length: field */
448	      continue;
449	   case '\n':case '\0':;		      /* ok, end of the line */
450	 }
451	break;
452      }
453     if(cntlen<=0)			       /* any Content-Length at all? */
454	cntlen=0;
455     ultstr(places,cntlen,num);			       /* our preferred size */
456     if(!num[places])		       /* does it fit in the existing space? */
457	tmemmove(themail.p+contlengthoffset,num,places),actcntlen=cntlen;
458     chp=thebody+actcntlen;		  /* skip the actual no we specified */
459   }
460  restbody=chp;mailread=1;
461}
462