chiark / gitweb /
better stats for missing
[inn-innduct.git] / frontends / inews.c
1 /*  $Id: inews.c 7769 2008-04-13 08:11:41Z iulius $
2 **
3 **  Send an article (prepared by someone on the local site) to the
4 **  master news server.
5 */
6
7 #include "config.h"
8 #include "clibrary.h"
9 #include "portable/time.h"
10 #include <ctype.h>
11 #include <errno.h>
12 #include <fcntl.h>
13 #include <grp.h>
14 #include <pwd.h>
15 #include <sys/stat.h>
16
17 #include "inn/innconf.h"
18 #include "inn/messages.h"
19 #include "libinn.h"
20 #include "nntp.h"
21 #include "paths.h"
22
23 /* Signature handling.  The separator will be appended before the signature,
24    and at most SIG_MAXLINES will be appended. */
25 #define SIG_MAXLINES            4
26 #define SIG_SEPARATOR           "-- \n"
27
28 #define FLUSH_ERROR(F)          (fflush((F)) == EOF || ferror((F)))
29 #define LPAREN                  '('     /* For vi :-) */
30 #define HEADER_DELTA            20
31 #define GECOSTERM(c)            \
32             ((c) == ',' || (c) == ';' || (c) == ':' || (c) == LPAREN)
33 #define HEADER_STRLEN           998
34
35 typedef enum _HEADERTYPE {
36     HTobs,
37     HTreq,
38     HTstd
39 } HEADERTYPE;
40
41 typedef struct _HEADER {
42     const char *Name;
43     bool        CanSet;
44     HEADERTYPE  Type;
45     int         Size;
46     char        *Value;
47 } HEADER;
48
49 static bool     Dump;
50 static bool     Revoked;
51 static bool     Spooling;
52 static char     **OtherHeaders;
53 static char     SIGSEP[] = SIG_SEPARATOR;
54 static FILE     *FromServer;
55 static FILE     *ToServer;
56 static int      OtherCount;
57 static int      OtherSize;
58 static const char *Exclusions = "";
59 static const char * const BadDistribs[] = {
60     BAD_DISTRIBS
61 };
62
63 static HEADER   Table[] = {
64     /*  Name                    Canset  Type    */
65     {   "Path",                 true,   HTstd,  0, NULL },
66 #define _path            0
67     {   "From",                 true,   HTstd,  0, NULL },
68 #define _from            1
69     {   "Newsgroups",           true,   HTreq,  0, NULL },
70 #define _newsgroups      2
71     {   "Subject",              true,   HTreq,  0, NULL },
72 #define _subject         3
73     {   "Control",              true,   HTstd,  0, NULL },
74 #define _control         4
75     {   "Supersedes",           true,   HTstd,  0, NULL },
76 #define _supersedes      5
77     {   "Followup-To",          true,   HTstd,  0, NULL },
78 #define _followupto      6
79     {   "Date",                 true,   HTstd,  0, NULL },
80 #define _date            7
81     {   "Organization",         true,   HTstd,  0, NULL },
82 #define _organization    8
83     {   "Lines",                true,   HTstd,  0, NULL },
84 #define _lines           9
85     {   "Sender",               true,   HTstd,  0, NULL },
86 #define _sender         10
87     {   "Approved",             true,   HTstd,  0, NULL },
88 #define _approved       11
89     {   "Distribution",         true,   HTstd,  0, NULL },
90 #define _distribution   12
91     {   "Expires",              true,   HTstd,  0, NULL },
92 #define _expires        13
93     {   "Message-ID",           true,   HTstd,  0, NULL },
94 #define _messageid      14
95     {   "References",           true,   HTstd,  0, NULL },
96 #define _references     15
97     {   "Reply-To",             true,   HTstd,  0, NULL },
98 #define _replyto        16
99     {   "Also-Control",         true,   HTstd,  0, NULL },
100 #define _alsocontrol    17
101     {   "Xref",                 false,  HTstd,  0, NULL },
102     {   "Summary",              true,   HTstd,  0, NULL },
103     {   "Keywords",             true,   HTstd,  0, NULL },
104     {   "Date-Received",        false,  HTobs,  0, NULL },
105     {   "Received",             false,  HTobs,  0, NULL },
106     {   "Posted",               false,  HTobs,  0, NULL },
107     {   "Posting-Version",      false,  HTobs,  0, NULL },
108     {   "Relay-Version",        false,  HTobs,  0, NULL },
109 };
110
111 #define HDR(_x) (Table[(_x)].Value)
112
113 \f
114
115 /*
116 **  Send the server a quit message, wait for a reply.
117 */
118 static void
119 QuitServer(int x)
120 {
121     char        buff[HEADER_STRLEN];
122     char        *p;
123
124     if (Spooling)
125         exit(x);
126     if (x)
127         warn("article not posted");
128     fprintf(ToServer, "quit\r\n");
129     if (FLUSH_ERROR(ToServer))
130         sysdie("cannot send quit to server");
131     if (fgets(buff, sizeof buff, FromServer) == NULL)
132         sysdie("warning: server did not reply to quit");
133     if ((p = strchr(buff, '\r')) != NULL)
134         *p = '\0';
135     if ((p = strchr(buff, '\n')) != NULL)
136         *p = '\0';
137     if (atoi(buff) != NNTP_GOODBYE_ACK_VAL)
138         die("server did not reply to quit properly: %s", buff);
139     fclose(FromServer);
140     fclose(ToServer);
141     exit(x);
142 }
143
144
145 /*
146 **  Failure handler, called by die.  Calls QuitServer to cleanly shut down the
147 **  connection with the remote server before exiting.
148 */
149 static int
150 fatal_cleanup(void)
151 {
152     /* Don't recurse. */
153     message_fatal_cleanup = NULL;
154
155     /* QuitServer does all the work. */
156     QuitServer(1);
157     return 1;
158 }
159
160
161 /*
162 **  Flush a stdio FILE; exit if there are any errors.
163 */
164 static void
165 SafeFlush(FILE *F)
166 {
167     if (FLUSH_ERROR(F))
168         sysdie("cannot send text to server");
169 }
170
171
172 /*
173 **  Trim trailing spaces, return pointer to first non-space char.
174 */
175 static char *
176 TrimSpaces(char *p)
177 {
178     char        *start;
179
180     for (start = p; ISWHITE(*start); start++)
181         continue;
182     for (p = start + strlen(start); p > start && CTYPE(isspace, p[-1]); )
183         *--p = '\0';
184     return start;
185 }
186
187
188 /*
189 **  Mark the end of the header starting at p, and return a pointer
190 **  to the start of the next one.  Handles continuations.
191 */
192 static char *
193 NextHeader(char *p)
194 {
195     for ( ; ; p++) {
196         if ((p = strchr(p, '\n')) == NULL)
197             die("article is all headers");
198         if (!ISWHITE(p[1])) {
199             *p = '\0';
200             return p + 1;
201         }
202     }
203 }
204
205
206 /*
207 **  Strip any headers off the article and dump them into the table.
208 */
209 static char *
210 StripOffHeaders(char *article)
211 {
212     char        *p;
213     char        *q;
214     HEADER      *hp;
215     char        c;
216     int i;
217
218     /* Set up the other headers list. */
219     OtherSize = HEADER_DELTA;
220     OtherHeaders = xmalloc(OtherSize * sizeof(char *));
221     OtherCount = 0;
222
223     /* Scan through buffer, a header at a time. */
224     for (i = 0, p = article; ; i++) {
225
226         if ((q = strchr(p, ':')) == NULL)
227             die("no colon in header line \"%.30s...\"", p);
228         if (q[1] == '\n' && !ISWHITE(q[2])) {
229             /* Empty header; ignore this one, get next line. */
230             p = NextHeader(p);
231             if (*p == '\n')
232                 break;
233         }
234
235         if (q[1] != '\0' && !ISWHITE(q[1])) {
236             if ((q = strchr(q, '\n')) != NULL)
237                 *q = '\0';
238             die("no space after colon in \"%.30s...\"", p);
239         }
240
241         /* See if it's a known header. */
242         c = CTYPE(islower, *p) ? toupper(*p) : *p;
243         for (hp = Table; hp < ARRAY_END(Table); hp++)
244             if (c == hp->Name[0]
245              && p[hp->Size] == ':'
246              && ISWHITE(p[hp->Size + 1])
247              && strncasecmp(p, hp->Name, hp->Size) == 0) {
248                 if (hp->Type == HTobs)
249                     die("obsolete header: %s", hp->Name);
250                 if (hp->Value)
251                     die("duplicate header: %s", hp->Name);
252                 for (q = &p[hp->Size + 1]; ISWHITE(*q); q++)
253                     continue;
254                 hp->Value = q;
255                 break;
256             }
257
258         /* Too many headers? */
259         if (++i > 5 * HEADER_DELTA)
260             die("more than %d lines of header", i);
261
262         /* No; add it to the set of other headers. */
263         if (hp == ARRAY_END(Table)) {
264             if (OtherCount >= OtherSize - 1) {
265                 OtherSize += HEADER_DELTA;
266                 OtherHeaders = xrealloc(OtherHeaders, OtherSize * sizeof(char *));
267             }
268             OtherHeaders[OtherCount++] = p;
269         }
270
271         /* Get start of next header; if it's a blank line, we hit the end. */
272         p = NextHeader(p);
273         if (*p == '\n')
274             break;
275     }
276
277     return p + 1;
278 }
279
280 \f
281
282 /*
283 **  See if the user is allowed to cancel the indicated message.  Assumes
284 **  that the Sender or From line has already been filled in.
285 */
286 static void
287 CheckCancel(char *msgid, bool JustReturn)
288 {
289     char                localfrom[SMBUF];
290     char        *p;
291     char                buff[BUFSIZ];
292     char                remotefrom[SMBUF];
293
294     /* Ask the server for the article. */
295     fprintf(ToServer, "head %s\r\n", msgid);
296     SafeFlush(ToServer);
297     if (fgets(buff, sizeof buff, FromServer) == NULL
298      || atoi(buff) != NNTP_HEAD_FOLLOWS_VAL) {
299         if (JustReturn)
300             return;
301         die("server has no such article");
302     }
303
304     /* Read the headers, looking for the From or Sender. */
305     remotefrom[0] = '\0';
306     while (fgets(buff, sizeof buff, FromServer) != NULL) {
307         if ((p = strchr(buff, '\r')) != NULL)
308             *p = '\0';
309         if ((p = strchr(buff, '\n')) != NULL)
310             *p = '\0';
311         if (buff[0] == '.' && buff[1] == '\0')
312             break;
313         if (strncmp(buff, "Sender:", 7) == 0)
314             strlcpy(remotefrom, TrimSpaces(&buff[7]), SMBUF);
315         else if (remotefrom[0] == '\0' && strncmp(buff, "From:", 5) == 0)
316             strlcpy(remotefrom, TrimSpaces(&buff[5]), SMBUF);
317     }
318     if (remotefrom[0] == '\0') {
319         if (JustReturn)
320             return;
321         die("article is garbled");
322     }
323     HeaderCleanFrom(remotefrom);
324
325     /* Get the local user. */
326     strlcpy(localfrom, HDR(_sender) ? HDR(_sender) : HDR(_from), SMBUF);
327     HeaderCleanFrom(localfrom);
328
329     /* Is the right person cancelling? */
330     if (strcasecmp(localfrom, remotefrom) != 0)
331         die("article was posted by \"%s\" and you are \"%s\"", remotefrom,
332             localfrom);
333 }
334
335
336 /*
337 **  See if the user is the news administrator.
338 */
339 static bool
340 AnAdministrator(char *name, gid_t group)
341 {
342     struct passwd       *pwp;
343     struct group        *grp;
344     char                **mem;
345     char                *p;
346
347     if (Revoked)
348         return false;
349
350     /* Find out who we are. */
351     if ((pwp = getpwnam(NEWSUSER)) == NULL)
352         /* Silent falure; clients might not have the group. */
353         return false;
354     if (getuid() == pwp->pw_uid)
355         return true;
356
357     /* See if the we're in the right group. */
358     if ((grp = getgrnam(NEWSGRP)) == NULL || (mem = grp->gr_mem) == NULL)
359         /* Silent falure; clients might not have the group. */
360         return false;
361     if (group == grp->gr_gid)
362         return true;
363     while ((p = *mem++) != NULL)
364         if (strcmp(name, p) == 0)
365             return true;
366     return false;
367 }
368
369
370 /*
371 **  Check the control message, and see if it's legit.
372 */
373 static void
374 CheckControl(char *ctrl, struct passwd *pwp)
375 {
376     char        *p;
377     char        *q;
378     char                save;
379     char                name[SMBUF];
380
381     /* Snip off the first word. */
382     for (p = ctrl; ISWHITE(*p); p++)
383         continue;
384     for (ctrl = p; *p && !ISWHITE(*p); p++)
385         continue;
386     if (p == ctrl)
387         die("emtpy control message");
388     save = *p;
389     *p = '\0';
390
391     if (strcmp(ctrl, "cancel") == 0) {
392         for (q = p + 1; ISWHITE(*q); q++)
393             continue;
394         if (*q == '\0')
395             die("message ID missing in cancel");
396         if (!Spooling)
397             CheckCancel(q, false);
398     }
399     else if (strcmp(ctrl, "checkgroups") == 0
400           || strcmp(ctrl, "ihave")       == 0
401           || strcmp(ctrl, "sendme")      == 0
402           || strcmp(ctrl, "newgroup")    == 0
403           || strcmp(ctrl, "rmgroup")     == 0
404           || strcmp(ctrl, "sendsys")     == 0
405           || strcmp(ctrl, "senduuname")  == 0
406           || strcmp(ctrl, "version")     == 0) {
407         strlcpy(name, pwp->pw_name, SMBUF);
408         if (!AnAdministrator(name, pwp->pw_gid))
409             die("ask your news administrator to do the %s for you", ctrl);
410     }
411     else {
412         die("%s is not a valid control message", ctrl);
413     }
414     *p = save;
415 }
416
417 \f
418
419 /*
420 **  Parse the GECOS field to get the user's full name.  This comes Sendmail's
421 **  buildfname routine.  Ignore leading stuff like "23-" "stuff]-" or
422 **  "stuff -" as well as trailing whitespace, or anything that comes after
423 **  a comma, semicolon, or in parentheses.  This seems to strip off most of
424 **  the UCB or ATT stuff people fill out the entries with.  Also, turn &
425 **  into the login name, with perhaps an initial capital.  (Everyone seems
426 **  to hate that, but everyone also supports it.)
427 */
428 static char *
429 FormatUserName(struct passwd *pwp, char *node)
430 {
431     char        outbuff[SMBUF];
432     char        *buff;
433     char        *out;
434     char        *p;
435     int         left;
436
437 #if     !defined(DONT_MUNGE_GETENV)
438     memset(outbuff, 0, SMBUF);
439     if ((p = getenv("NAME")) != NULL)
440         strlcpy(outbuff, p, SMBUF);
441     if (strlen(outbuff) == 0) {
442 #endif  /* !defined(DONT_MUNGE_GETENV) */
443
444
445 #ifndef DO_MUNGE_GECOS
446     strlcpy(outbuff, pwp->pw_gecos, SMBUF);
447 #else
448     /* Be very careful here.  If we're not, we can potentially overflow our
449      * buffer.  Remember that on some Unix systems, the content of the GECOS
450      * field is under (untrusted) user control and we could be setgid. */
451     p = pwp->pw_gecos;
452     left = SMBUF - 1;
453     if (*p == '*')
454         p++;
455     for (out = outbuff; *p && !GECOSTERM(*p) && left; p++) {
456         if (*p == '&') {
457             strncpy(out, pwp->pw_name, left);
458             if (CTYPE(islower, *out)
459              && (out == outbuff || !CTYPE(isalpha, out[-1])))
460                 *out = toupper(*out);
461             while (*out) {
462                 out++;
463                 left--;
464             }
465         }
466         else if (*p == '-'
467               && p > pwp->pw_gecos
468               && (CTYPE(isdigit, p[-1]) || CTYPE(isspace, p[-1])
469                   || p[-1] == ']')) {
470             out = outbuff;
471             left = SMBUF - 1;
472         }
473         else {
474             *out++ = *p;
475             left--;
476         }
477     }
478     *out = '\0';
479 #endif /* DO_MUNGE_GECOS */
480
481 #if     !defined(DONT_MUNGE_GETENV)
482     }
483 #endif  /* !defined(DONT_MUNGE_GETENV) */
484
485     out = TrimSpaces(outbuff);
486     if (out[0])
487         buff = concat(pwp->pw_name, "@", node, " (", out, ")", (char *) 0);
488     else
489         buff = concat(pwp->pw_name, "@", node, (char *) 0);
490     return buff;
491 }
492
493
494 /*
495 **  Check the Distribution header, and exit on error.
496 */
497 static void CheckDistribution(char *p)
498 {
499     static char SEPS[] = " \t,";
500     const char  * const *dp;
501
502     if ((p = strtok(p, SEPS)) == NULL)
503         die("cannot parse Distribution header");
504     do {
505         for (dp = BadDistribs; *dp; dp++)
506             if (uwildmat(p, *dp))
507                 die("illegal distribution %s", p);
508     } while ((p = strtok((char *)NULL, SEPS)) != NULL);
509 }
510
511
512 /*
513 **  Process all the headers.  FYI, they're done in RFC-order.
514 */
515 static void
516 ProcessHeaders(bool AddOrg, int linecount, struct passwd *pwp)
517 {
518     static char         PATHFLUFF[] = PATHMASTER;
519     HEADER              *hp;
520     char                *p;
521     TIMEINFO            Now;
522     char                buff[SMBUF];
523     char                from[SMBUF];
524
525     /* Do some preliminary fix-ups. */
526     for (hp = Table; hp < ARRAY_END(Table); hp++) {
527         if (!hp->CanSet && hp->Value)
528             die("cannot set system header %s", hp->Name);
529         if (hp->Value) {
530             hp->Value = TrimSpaces(hp->Value);
531             if (*hp->Value == '\0')
532                 hp->Value = NULL;
533         }
534     }
535
536     /* Set From or Sender. */
537     if ((p = innconf->fromhost) == NULL)
538         sysdie("cannot get hostname");
539     if (HDR(_from) == NULL)
540         HDR(_from) = FormatUserName(pwp, p);
541     else {
542       if (strlen(pwp->pw_name) + strlen(p) + 2 > sizeof(buff))
543           die("username and host are too long");
544       sprintf(buff, "%s@%s", pwp->pw_name, p);
545       strlcpy(from, HDR(_from), SMBUF);
546       HeaderCleanFrom(from);
547       if (strcmp(from, buff) != 0)
548         HDR(_sender) = xstrdup(buff);
549     }
550
551     if (HDR(_date) == NULL) {
552         /* Set Date. */
553         if (!makedate(-1, true, buff, sizeof(buff)))
554             die("cannot generate Date header");
555         HDR(_date) = xstrdup(buff);
556     }
557
558     /* Newsgroups are checked later. */
559
560     /* Set Subject; Control overrides the subject. */
561     if (HDR(_control)) {
562         CheckControl(HDR(_control), pwp);
563     }
564     else {
565         p = HDR(_subject);
566         if (p == NULL)
567             die("required Subject header is missing or empty");
568         else if (HDR(_alsocontrol))
569             CheckControl(HDR(_alsocontrol), pwp);
570 #if     0
571         if (strncmp(p, "Re: ", 4) == 0 && HDR(_references) == NULL)
572             die("article subject begins with \"Re: \" but has no references");
573 #endif  /* 0 */
574     }
575
576     /* Set Message-ID */
577     if (HDR(_messageid) == NULL) {
578         if ((p = GenerateMessageID(innconf->domain)) == NULL)
579             die("cannot generate Message-ID header");
580         HDR(_messageid) = xstrdup(p);
581     }
582     else if ((p = strchr(HDR(_messageid), '@')) == NULL
583              || strchr(++p, '@') != NULL) {
584         die("message ID must have exactly one @");
585     }
586
587     /* Set Path */
588     if (HDR(_path) == NULL) {
589 #if     defined(DO_INEWS_PATH)
590         if ((p = innconf->pathhost) != NULL) {
591             if (*p)
592                 HDR(_path) = concat(Exclusions, p, "!", PATHFLUFF, (char *) 0);
593             else
594                 HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
595         }
596         else if (innconf->server != NULL) {
597             if ((p = GetFQDN(innconf->domain)) == NULL)
598                 sysdie("cannot get hostname");
599             HDR(_path) = concat(Exclusions, p, "!", PATHFLUFF, (char *) 0);
600         }
601         else {
602             HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
603         }
604 #else
605         HDR(_path) = concat(Exclusions, PATHFLUFF, (char *) 0);
606 #endif  /* defined(DO_INEWS_PATH) */
607     }
608
609     /* Reply-To; left alone. */
610     /* Sender; set above. */
611     /* Followup-To; checked with Newsgroups. */
612
613     /* Check Expires. */
614     if (GetTimeInfo(&Now) < 0)
615         sysdie("cannot get the time");
616     if (HDR(_expires) && parsedate(HDR(_expires), &Now) == -1)
617         die("cannot parse \"%s\" as an expiration date", HDR(_expires));
618
619     /* References; left alone. */
620     /* Control; checked above. */
621
622     /* Distribution. */
623     if ((p = HDR(_distribution)) != NULL) {
624         p = xstrdup(p);
625         CheckDistribution(p);
626         free(p);
627     }
628
629     /* Set Organization. */
630     if (AddOrg
631      && HDR(_organization) == NULL
632      && (p = innconf->organization) != NULL) {
633         HDR(_organization) = xstrdup(p);
634     }
635
636     /* Keywords; left alone. */
637     /* Summary; left alone. */
638     /* Approved; left alone. */
639
640     /* Set Lines */
641     sprintf(buff, "%d", linecount);
642     HDR(_lines) = xstrdup(buff);
643
644     /* Check Supersedes. */
645     if (HDR(_supersedes))
646         CheckCancel(HDR(_supersedes), true);
647
648     /* Now make sure everything is there. */
649     for (hp = Table; hp < ARRAY_END(Table); hp++)
650         if (hp->Type == HTreq && hp->Value == NULL)
651             die("required header %s is missing or empty", hp->Name);
652 }
653
654
655 /*
656 **  Try to append $HOME/.signature to the article.  When in doubt, exit
657 **  out in order to avoid postings like "Sorry, I forgot my .signature
658 **  -- here's the article again."
659 */
660 static char *
661 AppendSignature(bool UseMalloc, char *article, char *homedir, int *linesp)
662 {
663     static char NOSIG[] = "Can't add your .signature (%s), article not posted";
664     int         i;
665     int         length;
666     size_t      artsize;
667     char        *p;
668     char        buff[BUFSIZ];
669     FILE        *F;
670
671     /* Open the file. */
672     *linesp = 0;
673     if (strlen(homedir) > sizeof(buff) - 14)
674         die("home directory path too long");
675     sprintf(buff, "%s/.signature", homedir);
676     if ((F = fopen(buff, "r")) == NULL) {
677         if (errno == ENOENT)
678             return article;
679         fprintf(stderr, NOSIG, strerror(errno));
680         QuitServer(1);
681     }
682
683     /* Read it in. */
684     length = fread(buff, 1, sizeof buff - 2, F);
685     i = feof(F);
686     fclose(F);
687     if (length == 0)
688         die("signature file is empty");
689     if (length < 0)
690         sysdie("cannot read signature file");
691     if (length == sizeof buff - 2 && !i)
692         die("signature is too large");
693
694     /* Make sure the buffer ends with \n\0. */
695     if (buff[length - 1] != '\n')
696         buff[length++] = '\n';
697     buff[length] = '\0';
698
699     /* Count the lines. */
700     for (i = 0, p = buff; (p = strchr(p, '\n')) != NULL; p++)
701         if (++i > SIG_MAXLINES)
702             die("signature has too many lines");
703     *linesp = 1 + i;
704
705     /* Grow the article to have the signature. */
706     i = strlen(article);
707     artsize = i + sizeof(SIGSEP) - 1 + length + 1;
708     if (UseMalloc) {
709         p = xmalloc(artsize);
710         strlcpy(p, article, artsize);
711         article = p;
712     }
713     else
714         article = xrealloc(article, artsize);
715     strlcat(article, SIGSEP, artsize);
716     strlcat(article, buff, artsize);
717     return article;
718 }
719
720
721 /*
722 **  See if the user has more included text than new text.  Simple-minded, but
723 **  reasonably effective for catching neophyte's mistakes.  A line starting
724 **  with > is included text.  Decrement the count on lines starting with <
725 **  so that we don't reject diff(1) output.
726 */
727 static void
728 CheckIncludedText(char *p, int lines)
729 {
730     int i;
731
732     for (i = 0; ; p++) {
733         switch (*p) {
734         case '>':
735             i++;
736             break;
737         case '|':
738             i++;
739             break;
740         case ':':
741             i++;
742             break;
743         case '<':
744             i--;
745             break;
746         }
747         if ((p = strchr(p, '\n')) == NULL)
748             break;
749     }
750     if ((i * 2 > lines) && (lines > 40))
751         die("more included text than new text");
752 }
753
754 \f
755
756 /*
757 **  Read stdin into a string and return it.  Can't use ReadInDescriptor
758 **  since that will fail if stdin is a tty.
759 */
760 static char *
761 ReadStdin(void)
762 {
763     int size;
764     char        *p;
765     char                *article;
766     char        *end;
767     int i;
768
769     size = BUFSIZ;
770     article = xmalloc(size);
771     end = &article[size - 3];
772     for (p = article; (i = getchar()) != EOF; *p++ = (char)i)
773         if (p == end) {
774             article = xrealloc(article, size + BUFSIZ);
775             p = &article[size - 3];
776             size += BUFSIZ;
777             end = &article[size - 3];
778         }
779
780     /* Force a \n terminator. */
781     if (p > article && p[-1] != '\n')
782         *p++ = '\n';
783     *p = '\0';
784     return article;
785 }
786
787 \f
788
789 /*
790 **  Offer the article to the server, return its reply.
791 */
792 static int
793 OfferArticle(char *buff, bool Authorized)
794 {
795     fprintf(ToServer, "post\r\n");
796     SafeFlush(ToServer);
797     if (fgets(buff, HEADER_STRLEN, FromServer) == NULL)
798         sysdie(Authorized ? "Can't offer article to server (authorized)"
799                           : "Can't offer article to server");
800     return atoi(buff);
801 }
802
803
804 /*
805 **  Spool article to temp file.
806 */
807 static void
808 Spoolit(char *article, size_t Length, char *deadfile)
809 {
810     HEADER *hp;
811     FILE *F;
812     int i;
813
814     /* Try to write to the deadfile. */
815     if (deadfile == NULL)
816         return;
817     F = xfopena(deadfile);
818     if (F == NULL)
819         sysdie("cannot create spool file");
820
821     /* Write the headers and a blank line. */
822     for (hp = Table; hp < ARRAY_END(Table); hp++)
823         if (hp->Value)
824             fprintf(F, "%s: %s\n", hp->Name, hp->Value);
825     for (i = 0; i < OtherCount; i++)
826         fprintf(F, "%s\n", OtherHeaders[i]);
827     fprintf(F, "\n");
828     if (FLUSH_ERROR(F))
829         sysdie("cannot write headers");
830
831     /* Write the article and exit. */
832     if (fwrite(article, 1, Length, F) != Length)
833         sysdie("cannot write article");
834     if (FLUSH_ERROR(F))
835         sysdie("cannot write article");
836     if (fclose(F) == EOF)
837         sysdie("cannot close spool file");
838 }
839
840
841 /*
842 **  Print usage message and exit.
843 */
844 static void
845 Usage(void)
846 {
847     fprintf(stderr, "Usage: inews [-D] [-h] [header_flags] [article]\n");
848     /* Don't call QuitServer here -- connection isn't open yet. */
849     exit(1);
850 }
851
852
853 int
854 main(int ac, char *av[])
855 {
856     static char         NOCONNECT[] = "cannot connect to server";
857     int                 i;
858     char                *p;
859     HEADER              *hp;
860     int                 j;
861     int                 port;
862     int                 Mode;
863     int                 SigLines;
864     struct passwd       *pwp;
865     char                *article;
866     char                *deadfile;
867     char                buff[HEADER_STRLEN];
868     char                SpoolMessage[HEADER_STRLEN];
869     bool                DoSignature;
870     bool                AddOrg;
871     size_t              Length;
872     uid_t               uid;
873
874     /* First thing, set up logging and our identity. */
875     message_program_name = "inews";
876
877     /* Find out who we are. */
878     uid = geteuid();
879     if (uid == (uid_t) -1)
880         sysdie("cannot get your user ID");
881     if ((pwp = getpwuid(uid)) == NULL)
882         sysdie("cannot get your passwd entry");
883
884     /* Set defaults. */
885     Mode = '\0';
886     Dump = false;
887     DoSignature = true;
888     AddOrg = true;
889     port = 0;
890
891     if (!innconf_read(NULL))
892         exit(1);
893
894     umask(NEWSUMASK);
895
896     /* Parse JCL. */
897     while ((i = getopt(ac, av, "DNAVWORShx:a:c:d:e:f:n:p:r:t:F:o:w:")) != EOF)
898         switch (i) {
899         default:
900             Usage();
901             /* NOTREACHED */
902         case 'D':
903         case 'N':
904             Dump = true;
905             break;
906         case 'A':
907         case 'V':
908         case 'W':
909             /* Ignore C News options. */
910             break;
911         case 'O':
912             AddOrg = false;
913             break;
914         case 'R':
915             Revoked = true;
916             break;
917         case 'S':
918             DoSignature = false;
919             break;
920         case 'h':
921             Mode = i;
922             break;
923         case 'x':
924             Exclusions = concat(optarg, "!", (char *) 0);
925             break;
926          case 'p':
927             port = atoi(optarg);
928             break;
929         /* Header lines that can be specified on the command line. */
930         case 'a':       HDR(_approved) = optarg;                break;
931         case 'c':       HDR(_control) = optarg;                 break;
932         case 'd':       HDR(_distribution) = optarg;            break;
933         case 'e':       HDR(_expires) = optarg;                 break;
934         case 'f':       HDR(_from) = optarg;                    break;
935         case 'n':       HDR(_newsgroups) = optarg;              break;
936         case 'r':       HDR(_replyto) = optarg;                 break;
937         case 't':       HDR(_subject) = optarg;                 break;
938         case 'F':       HDR(_references) = optarg;              break;
939         case 'o':       HDR(_organization) = optarg;            break;
940         case 'w':       HDR(_followupto) = optarg;              break;
941         }
942     ac -= optind;
943     av += optind;
944
945     /* Parse positional arguments; at most one, the input file. */
946     switch (ac) {
947     default:
948         Usage();
949         /* NOTREACHED */
950     case 0:
951         /* Read stdin. */
952         article = ReadStdin();
953         break;
954     case 1:
955         /* Read named file. */
956         article = ReadInFile(av[0], (struct stat *)NULL);
957         if (article == NULL)
958             sysdie("cannot read input file");
959         break;
960     }
961
962     if (port == 0)
963         port = NNTP_PORT;
964
965     /* Try to open a connection to the server. */
966     if (NNTPremoteopen(port, &FromServer, &ToServer, buff) < 0) {
967         Spooling = true;
968         if ((p = strchr(buff, '\n')) != NULL)
969             *p = '\0';
970         if ((p = strchr(buff, '\r')) != NULL)
971             *p = '\0';
972         strcpy(SpoolMessage, buff[0] ? buff : NOCONNECT);
973         deadfile = concatpath(pwp->pw_dir, "dead.article");
974     }
975     else {
976         /* We now have an open server connection, so close it on failure. */
977         message_fatal_cleanup = fatal_cleanup;
978
979         /* See if we can post. */
980         i = atoi(buff);
981
982         /* Tell the server we're posting. */
983         setbuf(FromServer, xmalloc(BUFSIZ));
984         setbuf(ToServer, xmalloc(BUFSIZ));
985         fprintf(ToServer, "mode reader\r\n");
986         SafeFlush(ToServer);
987         if (fgets(buff, HEADER_STRLEN, FromServer) == NULL)
988             sysdie("cannot tell server we're reading");
989         if ((j = atoi(buff)) != NNTP_BAD_COMMAND_VAL)
990             i = j;
991
992         if (i != NNTP_POSTOK_VAL) {
993             /* We try to authenticate in case it is all the same possible
994              * to post. */
995             if (NNTPsendpassword((char *)NULL, FromServer, ToServer) < 0)
996                 die("you do not have permission to post");
997         }
998         deadfile = NULL;
999     }
1000
1001     /* Basic processing. */
1002     for (hp = Table; hp < ARRAY_END(Table); hp++)
1003         hp->Size = strlen(hp->Name);
1004     if (Mode == 'h')
1005         article = StripOffHeaders(article);
1006     for (i = 0, p = article; (p = strchr(p, '\n')) != NULL; i++, p++)
1007         continue;
1008     if (innconf->checkincludedtext)
1009         CheckIncludedText(article, i);
1010     if (DoSignature)
1011         article = AppendSignature(Mode == 'h', article, pwp->pw_dir, &SigLines);
1012     else
1013         SigLines = 0;
1014     ProcessHeaders(AddOrg, i + SigLines, pwp);
1015     Length = strlen(article);
1016     if ((innconf->localmaxartsize > 0)
1017             && (Length > (size_t)innconf->localmaxartsize))
1018         die("article is larger than local limit of %ld bytes",
1019             innconf->localmaxartsize);
1020
1021     /* Do final checks. */
1022     if (i == 0 && HDR(_control) == NULL)
1023         die("article is empty");
1024     for (hp = Table; hp < ARRAY_END(Table); hp++)
1025         if (hp->Value && (int)strlen(hp->Value) + hp->Size > HEADER_STRLEN)
1026             die("%s header is too long", hp->Name);
1027     for (i = 0; i < OtherCount; i++)
1028         if ((int)strlen(OtherHeaders[i]) > HEADER_STRLEN)
1029             die("header too long (maximum length is %d): %.40s...",
1030                 HEADER_STRLEN, OtherHeaders[i]);
1031
1032     if (Dump) {
1033         /* Write the headers and a blank line. */
1034         for (hp = Table; hp < ARRAY_END(Table); hp++)
1035             if (hp->Value)
1036                 printf("%s: %s\n", hp->Name, hp->Value);
1037         for (i = 0; i < OtherCount; i++)
1038             printf("%s\n", OtherHeaders[i]);
1039         printf("\n");
1040         if (FLUSH_ERROR(stdout))
1041             sysdie("cannot write headers");
1042
1043         /* Write the article and exit. */
1044         if (fwrite(article, 1, Length, stdout) != Length)
1045             sysdie("cannot write article");
1046         SafeFlush(stdout);
1047         QuitServer(0);
1048     }
1049
1050     if (Spooling) {
1051         warn("warning: %s", SpoolMessage);
1052         warn("article will be spooled");
1053         Spoolit(article, Length, deadfile);
1054         exit(0);
1055     }
1056
1057     /* Article is prepared, offer it to the server. */
1058     i = OfferArticle(buff, false);
1059     if (i == NNTP_AUTH_NEEDED_VAL) {
1060         /* Posting not allowed, try to authorize. */
1061         if (NNTPsendpassword((char *)NULL, FromServer, ToServer) < 0)
1062             sysdie("authorization error");
1063         i = OfferArticle(buff, true);
1064     }
1065     if (i != NNTP_START_POST_VAL)
1066         die("server doesn't want the article: %s", buff);
1067
1068     /* Write the headers, a blank line, then the article. */
1069     for (hp = Table; hp < ARRAY_END(Table); hp++)
1070         if (hp->Value)
1071             fprintf(ToServer, "%s: %s\r\n", hp->Name, hp->Value);
1072     for (i = 0; i < OtherCount; i++)
1073         fprintf(ToServer, "%s\r\n", OtherHeaders[i]);
1074     fprintf(ToServer, "\r\n");
1075     if (NNTPsendarticle(article, ToServer, true) < 0)
1076         sysdie("cannot send article to server");
1077     SafeFlush(ToServer);
1078
1079     if (fgets(buff, sizeof buff, FromServer) == NULL)
1080         sysdie("no reply from server after sending the article");
1081     if ((p = strchr(buff, '\r')) != NULL)
1082         *p = '\0';
1083     if ((p = strchr(buff, '\n')) != NULL)
1084         *p = '\0';
1085     if (atoi(buff) != NNTP_POSTEDOK_VAL)
1086         die("cannot send article to server: %s", buff);
1087
1088     /* Close up. */
1089     QuitServer(0);
1090     /* NOTREACHED */
1091     return 1;
1092 }