chiark / gitweb /
wip compile
[inn-innduct.git] / nnrpd / post.c
1 /*  $Revision: 7450 $
2 **
3 **  Check article, send it to the local server.
4 */
5 #include "config.h"
6 #include "clibrary.h"
7
8 #include "inn/innconf.h"
9 #include "nnrpd.h"
10 #include "ov.h"
11 #include "post.h"
12
13 #define FLUSH_ERROR(F)          (fflush((F)) == EOF || ferror((F)))
14 #define HEADER_DELTA            20
15
16 static char     *tmpPtr ;
17 static char     Error[SMBUF];
18 static char     NGSEPS[] = NG_SEPARATOR;
19 char    **OtherHeaders;
20 int     OtherCount;
21 bool   HeadersModified;
22 static int      OtherSize;
23 static const char * const BadDistribs[] = {
24     BAD_DISTRIBS
25 };
26
27 HEADER Table[] = {
28     /*  Name                    Canset  Type    Size  Value */
29     {   "Path",                 true,   HTstd,  0,    NULL,    NULL, 0 },
30     {   "From",                 true,   HTreq,  0,    NULL,    NULL, 0 },
31     {   "Newsgroups",           true,   HTreq,  0,    NULL,    NULL, 0 },
32     {   "Subject",              true,   HTreq,  0,    NULL,    NULL, 0 },
33     {   "Control",              true,   HTstd,  0,    NULL,    NULL, 0 },
34     {   "Supersedes",           true,   HTstd,  0,    NULL,    NULL, 0 },
35     {   "Followup-To",          true,   HTstd,  0,    NULL,    NULL, 0 },
36     {   "Date",                 true,   HTstd,  0,    NULL,    NULL, 0 },
37     {   "Organization",         true,   HTstd,  0,    NULL,    NULL, 0 },
38     {   "Lines",                true,   HTstd,  0,    NULL,    NULL, 0 },
39     {   "Sender",               true,   HTstd,  0,    NULL,    NULL, 0 },
40     {   "Approved",             true,   HTstd,  0,    NULL,    NULL, 0 },
41     {   "Distribution",         true,   HTstd,  0,    NULL,    NULL, 0 },
42     {   "Expires",              true,   HTstd,  0,    NULL,    NULL, 0 },
43     {   "Message-ID",           true,   HTstd,  0,    NULL,    NULL, 0 },
44     {   "References",           true,   HTstd,  0,    NULL,    NULL, 0 },
45     {   "Reply-To",             true,   HTstd,  0,    NULL,    NULL, 0 },
46     {   "NNTP-Posting-Host",    false,  HTstd,  0,    NULL,    NULL, 0 },
47     {   "Mime-Version",         true,   HTstd,  0,    NULL,    NULL, 0 },
48     {   "Content-Type",         true,   HTstd,  0,    NULL,    NULL, 0 },
49     {   "Content-Transfer-Encoding", true, HTstd,  0,    NULL,    NULL, 0 },
50     {   "X-Trace",              false,  HTstd,  0,    NULL,    NULL, 0 },
51     {   "X-Complaints-To",      false,  HTstd,  0,    NULL,    NULL, 0 },
52     {   "NNTP-Posting-Date",    false,  HTstd,  0,    NULL,    NULL, 0 },
53     {   "Xref",                 false,  HTstd,  0,    NULL,    NULL, 0 },
54     {   "Injector-Info",        false,  HTstd,  0,    NULL,    NULL, 0 },
55     {   "Summary",              true,   HTstd,  0,    NULL,    NULL, 0 },
56     {   "Keywords",             true,   HTstd,  0,    NULL,    NULL, 0 },
57     {   "Date-Received",        false,  HTobs,  0,    NULL,    NULL, 0 },
58     {   "Received",             false,  HTobs,  0,    NULL,    NULL, 0 },
59     {   "Posted",               false,  HTobs,  0,    NULL,    NULL, 0 },
60     {   "Posting-Version",      false,  HTobs,  0,    NULL,    NULL, 0 },
61     {   "Relay-Version",        false,  HTobs,  0,    NULL,    NULL, 0 },
62     {   "Cc",                   true, HTstd,  0,    NULL,    NULL, 0 },
63     {   "Bcc",                  true, HTstd,  0,    NULL,    NULL, 0 },
64     {   "To",                   true, HTstd,  0,    NULL,    NULL, 0 },
65 };
66
67 HEADER *EndOfTable = ARRAY_END(Table);
68
69 \f
70
71 /* Join() and MaxLength() are taken from innd.c */
72 /*
73 **  Turn any \r or \n in text into spaces.  Used to splice back multi-line
74 **  headers into a single line.
75 */
76 static char *
77 Join(char       *text)
78 {
79     char        *p;
80
81     for (p = text; *p; p++)
82         if (*p == '\n' || *p == '\r')
83             *p = ' ';
84     return text;
85 }
86
87 /*
88 **  Return a short name that won't overrun our bufer or syslog's buffer.
89 **  q should either be p, or point into p where the "interesting" part is.
90 */
91 static char *
92 MaxLength(char *p, char *q)
93 {
94     static char         buff[80];
95     unsigned int        i;
96
97     /* Already short enough? */
98     i = strlen(p);
99     if (i < sizeof buff - 1)
100         return Join(p);
101
102     /* Don't want casts to unsigned to go horribly wrong. */
103     if (q < p || q > p + i)
104         q = p;
105
106     /* Simple case of just want the begining? */
107     if ((size_t)(q - p) < sizeof(buff) - 4) {
108         strlcpy(buff, p, sizeof(buff) - 3);
109         strlcat(buff, "...", sizeof(buff));
110     } else if ((p + i) - q < 10) {
111         /* Is getting last 10 characters good enough? */
112         strlcpy(buff, p, sizeof(buff) - 13);
113         strlcat(buff, "...", sizeof(buff) - 10);
114         strlcat(buff, &p[i - 10], sizeof(buff));
115     } else {
116         /* Not in last 10 bytes, so use double elipses. */
117         strlcpy(buff, p, sizeof(buff) - 16);
118         strlcat(buff, "...", sizeof(buff) - 13);
119         strlcat(buff, &q[-5], sizeof(buff) - 3);
120         strlcat(buff, "...", sizeof(buff));
121     }
122     return Join(buff);
123 }
124 /*
125 **  Trim trailing spaces, return pointer to first non-space char.
126 */
127 int
128 TrimSpaces(char *p)
129 {
130     char        *start;
131
132     for (start = p; ISWHITE(*start) || *start == '\n'; start++)
133         continue;
134     for (p = start + strlen(start); p > start && CTYPE(isspace, (int)p[-1]); p--)
135         continue;
136     return (int)(p - start);
137 }
138
139
140 /*
141 **  Mark the end of the header starting at p, and return a pointer
142 **  to the start of the next one or NULL.  Handles continuations.
143 */
144 static char *
145 NextHeader(char *p)
146 {
147     for ( ; (p = strchr(p, '\n')) != NULL; p++) {
148         if (ISWHITE(p[1]))
149             continue;
150         *p = '\0';
151         return p + 1;
152     }
153     return NULL;
154 }
155
156
157 /*
158 **  Strip any headers off the article and dump them into the table.
159 **  On error, return NULL and fill in Error.
160 */
161 static char *
162 StripOffHeaders(char *article)
163 {
164     char        *p;
165     char        *q;
166     HEADER      *hp;
167     char        c;
168
169     /* Scan through buffer, a header at a time. */
170     for (p = article; ; ) {
171
172         /* See if it's a known header. */
173         c = CTYPE(islower, (int)*p) ? toupper(*p) : *p;
174         for (hp = Table; hp < ARRAY_END(Table); hp++) {
175             if (c == hp->Name[0]
176              && p[hp->Size] == ':'
177              && strncasecmp(p, hp->Name, hp->Size) == 0) {
178                 if (hp->Type == HTobs) {
179                     snprintf(Error, sizeof(Error), "Obsolete \"%s\" header",
180                              hp->Name);
181                     return NULL;
182                 }
183                 if (hp->Value) {
184                     snprintf(Error, sizeof(Error), "Duplicate \"%s\" header",
185                              hp->Name);
186                     return NULL;
187                 }
188                 hp->Value = &p[hp->Size + 1];
189                 /* '\r\n' is replaced with '\n', and unnecessary to consider
190                    '\r' */
191                 for (q = &p[hp->Size + 1]; ISWHITE(*q) || *q == '\n'; q++)
192                     continue;
193                 hp->Body = q;
194                 break;
195             }
196         }
197
198         /* No; add it to the set of other headers. */
199         if (hp == ARRAY_END(Table)) {
200             if (OtherCount >= OtherSize - 1) {
201                 OtherSize += HEADER_DELTA;
202                 OtherHeaders = xrealloc(OtherHeaders, OtherSize * sizeof(char *));
203             }
204             OtherHeaders[OtherCount++] = p;
205         }
206
207         /* Get start of next header; if it's a blank line, we hit the end. */
208         if ((p = NextHeader(p)) == NULL) {
209             strlcpy(Error, "Article has no body -- just headers",
210                     sizeof(Error));
211             return NULL;
212         }
213         if (*p == '\n')
214             break;
215     }
216
217     return p + 1;
218 }
219
220 \f
221
222 /*
223 **  Check the control message, and see if it's legit.  Return pointer to
224 **  error message if not.
225 */
226 static const char *
227 CheckControl(char *ctrl)
228 {
229     char        *p;
230     char        *q;
231     char        save;
232
233     /* Snip off the first word. */
234     for (p = ctrl; ISWHITE(*p); p++)
235         continue;
236     for (ctrl = p; *p && !ISWHITE(*p); p++)
237         continue;
238     if (p == ctrl)
239         return "Empty control message";
240     save = *p;
241     *p = '\0';
242
243     if (strcmp(ctrl, "cancel") == 0) {
244         for (q = p + 1; ISWHITE(*q); q++)
245             continue;
246         if (*q == '\0')
247             return "Message-ID missing in cancel";
248     }
249     else if (strcmp(ctrl, "sendsys")     == 0
250           || strcmp(ctrl, "senduuname")  == 0
251           || strcmp(ctrl, "version")     == 0
252           || strcmp(ctrl, "checkgroups") == 0
253           || strcmp(ctrl, "ihave")       == 0
254           || strcmp(ctrl, "sendme")      == 0
255           || strcmp(ctrl, "newgroup")    == 0
256           || strcmp(ctrl, "rmgroup")     == 0)
257         ;
258     else {
259         snprintf(Error, sizeof(Error),
260                  "\"%s\" is not a valid control message",
261                  MaxLength(ctrl,ctrl));
262         return Error;
263     }
264     *p = save;
265     return NULL;
266 }
267
268
269 /*
270 **  Check the Distribution header, and exit on error.
271 */
272 static const char *
273 CheckDistribution(char *p)
274 {
275     static char SEPS[] = " \t,";
276     const char * const *dp;
277
278     if ((p = strtok(p, SEPS)) == NULL)
279         return "Can't parse Distribution line.";
280     do {
281         for (dp = BadDistribs; *dp; dp++)
282             if (uwildmat(p, *dp)) {
283                 snprintf(Error, sizeof(Error), "Illegal distribution \"%s\"",
284                          MaxLength(p,p));
285                 return Error;
286             }
287     } while ((p = strtok((char *)NULL, SEPS)) != NULL);
288     return NULL;
289 }
290
291
292 /*
293 **  Process all the headers.  FYI, they're done in RFC-order.
294 **  Return NULL if okay, or an error message.
295 */
296 static const char *
297 ProcessHeaders(int linecount, char *idbuff, bool ihave)
298 {
299     static char         MONTHS[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
300     static char         datebuff[40];
301     static char         localdatebuff[40];
302     static char         orgbuff[SMBUF];
303     static char         linebuff[40];
304     static char         tracebuff[SMBUF];
305     static char         complaintsbuff[SMBUF];
306     static char         sendbuff[SMBUF];
307     static char         *newpath = NULL;
308     HEADER              *hp;
309     char                *p;
310     time_t              t;
311     struct tm           *gmt;
312     TIMEINFO            Now;
313     const char          *error;
314     pid_t               pid;
315     bool                addvirtual = false;
316
317     /* Various things need Now to be set. */
318     if (GetTimeInfo(&Now) < 0) {
319         snprintf(Error, sizeof(Error), "Can't get the time, %s",
320                  strerror(errno));
321         return Error;
322     }
323
324     /* Do some preliminary fix-ups. */
325     for (hp = Table; hp < ARRAY_END(Table); hp++) {
326         if (!ihave && !hp->CanSet && hp->Value) {
327             snprintf(Error, sizeof(Error),
328                      "Can't set system \"%s\" header", hp->Name);
329             return Error;
330         }
331         if (hp->Value) {
332             hp->Len = TrimSpaces(hp->Value);
333             if (hp->Len == 0)
334                 hp->Value = hp->Body = NULL;
335         }
336     }
337
338     /* If authorized, add the header based on our info.  If not authorized,
339        zap the Sender so we don't put out unauthenticated data. */
340     if (PERMaccessconf->nnrpdauthsender) {
341         if (PERMauthorized && PERMuser[0] != '\0') {
342             p = strchr(PERMuser, '@');
343             if (p == NULL) {
344                 snprintf(sendbuff, sizeof(sendbuff), "%s@%s", PERMuser,
345                          ClientHost);
346             } else {
347                 snprintf(sendbuff, sizeof(sendbuff), "%s", PERMuser);
348             }
349             HDR_SET(HDR__SENDER, sendbuff);
350         } else {
351             HDR_SET(HDR__SENDER, NULL);
352         }
353     }
354
355     /* Set Date.  datebuff is used later for NNTP-Posting-Date, so we have
356        to set it and it has to be the UTC date. */
357     if (!makedate(-1, false, datebuff, sizeof(datebuff)))
358         return "Can't generate date header";
359     if (HDR(HDR__DATE) == NULL) {
360         if (ihave)
361             return "Missing \"Date\" header";
362         if (PERMaccessconf->localtime) {
363             if (!makedate(-1, true, localdatebuff, sizeof(localdatebuff)))
364                 return "Can't generate local date header";
365             HDR_SET(HDR__DATE, localdatebuff);
366         } else {
367             HDR_SET(HDR__DATE, datebuff);
368         }
369     } else {
370         if ((t = parsedate(HDR(HDR__DATE), &Now)) == -1)
371             return "Can't parse \"Date\" header";
372         if (t > Now.time + DATE_FUZZ)
373             return "Article posted in the future";
374     }
375
376     /* Newsgroups are checked later. */
377
378     if (HDR(HDR__CONTROL)) {
379         if ((error = CheckControl(HDR(HDR__CONTROL))) != NULL)
380             return error;
381     } else {
382         p = HDR(HDR__SUBJECT);
383         if (p == NULL)
384             return "Required \"Subject\" header is missing";
385         if (strncmp(p, "cmsg ", 5) == 0) {
386             HDR_SET(HDR__CONTROL, p + 5);
387             if ((error = CheckControl(HDR(HDR__CONTROL))) != NULL)
388                 return error;
389         }
390     }
391
392     /* Set Message-ID */
393     if (HDR(HDR__MESSAGEID) == NULL) {
394         if (ihave)
395             return "Missing \"Message-ID\" header";
396         HDR_SET(HDR__MESSAGEID, idbuff);
397     }
398
399     /* Set Path */
400     if (HDR(HDR__PATH) == NULL) {
401         if (ihave)
402             return "Missing \"Path\" header";
403         /* Note that innd will put host name here for us. */
404         HDR_SET(HDR__PATH, PATHMASTER);
405         if (VirtualPathlen > 0)
406             addvirtual = true;
407     } else if (PERMaccessconf->strippath) {
408         /* Here's where to do Path changes for new Posts. */
409         if ((p = strrchr(HDR(HDR__PATH), '!')) != NULL) {
410             p++;
411             if (*p == '\0') {
412                 HDR_SET(HDR__PATH, PATHMASTER);
413                 if (VirtualPathlen > 0)
414                     addvirtual = true;
415             } else {
416                 HDR_SET(HDR__PATH, p);
417                 if ((VirtualPathlen > 0) &&
418                     strcmp(p, PERMaccessconf->pathhost) != 0)
419                     addvirtual = true;
420             }
421         } else if (VirtualPathlen > 0)
422             addvirtual = true;
423     } else {
424         if ((VirtualPathlen > 0) &&
425             (p = strchr(HDR(HDR__PATH), '!')) != NULL) {
426             *p = '\0';
427             if (strcmp(HDR(HDR__PATH), PERMaccessconf->pathhost) != 0)
428                 addvirtual = true;
429             *p = '!';
430         } else if (VirtualPathlen > 0)
431             addvirtual = true;
432     }
433     if (addvirtual) {
434         if (newpath != NULL)
435             free(newpath);
436         newpath = concat(VirtualPath, HDR(HDR__PATH), (char *) 0);
437         HDR_SET(HDR__PATH, newpath);
438     }
439     
440
441     /* Reply-To; left alone. */
442     /* Sender; set above. */
443
444     /* Check Expires. */
445     if (HDR(HDR__EXPIRES) && parsedate(HDR(HDR__EXPIRES), &Now) == -1)
446         return "Can't parse \"Expires\" header";
447
448     /* References; left alone. */
449     /* Control; checked above. */
450
451     /* Distribution. */
452     if ((p = HDR(HDR__DISTRIBUTION)) != NULL) {
453         p = xstrdup(p);
454         error = CheckDistribution(p);
455         free(p);
456         if (error != NULL)
457             return error;
458     }
459
460     /* Set Organization */
461     if (!ihave && HDR(HDR__ORGANIZATION) == NULL
462      && (p = PERMaccessconf->organization) != NULL) {
463         strlcpy(orgbuff, p, sizeof(orgbuff));
464         HDR_SET(HDR__ORGANIZATION, orgbuff);
465     }
466
467     /* Keywords; left alone. */
468     /* Summary; left alone. */
469     /* Approved; left alone. */
470
471     /* Set Lines */
472     if (!ihave) {
473         snprintf(linebuff, sizeof(linebuff), "%d", linecount);
474         HDR_SET(HDR__LINES, linebuff);
475     }
476
477     /* Supersedes; left alone. */
478
479     /* NNTP-Posting host; set. */
480     if (!ihave && PERMaccessconf->addnntppostinghost) 
481     HDR_SET(HDR__NNTPPOSTINGHOST, ClientHost);
482     /* NNTP-Posting-Date - not in RFC (yet) */
483     if (!ihave && PERMaccessconf->addnntppostingdate)
484     HDR_SET(HDR__NNTPPOSTINGDATE, datebuff);
485
486     /* X-Trace; set */
487     t = time((time_t *)NULL) ;
488     pid = (long) getpid() ;
489     if ((gmt = gmtime(&Now.time)) == NULL)
490         return "Can't get the time";
491     if (VirtualPathlen > 0)
492         p = PERMaccessconf->domain;
493     else
494         if ((p = GetFQDN(PERMaccessconf->domain)) == NULL)
495             p = "unknown";
496     snprintf(tracebuff, sizeof(tracebuff),
497              "%s %ld %ld %s (%d %3.3s %d %02d:%02d:%02d GMT)",
498              p, (long) t, (long) pid, ClientIpString,
499              gmt->tm_mday, &MONTHS[3 * gmt->tm_mon], 1900 + gmt->tm_year,
500              gmt->tm_hour, gmt->tm_min, gmt->tm_sec);
501     HDR_SET(HDR__XTRACE, tracebuff);
502
503     /* X-Complaints-To; set */
504     if ((p = PERMaccessconf->complaints) != NULL)
505         snprintf (complaintsbuff, sizeof(complaintsbuff), "%s", p);
506     else {
507         static const char newsmaster[] = NEWSMASTER;
508
509         if ((p = PERMaccessconf->fromhost) != NULL && strchr(newsmaster, '@') == NULL)
510             snprintf (complaintsbuff, sizeof(complaintsbuff), "%s@%s",
511                       newsmaster, p);
512         else
513             snprintf (complaintsbuff, sizeof(complaintsbuff), "%s",
514                       newsmaster);
515     }
516     HDR_SET(HDR__XCOMPLAINTSTO, complaintsbuff);
517
518     /* Clear out some headers that should not be here */
519     if (!ihave && PERMaccessconf->strippostcc) {
520         HDR_SET(HDR__CC, NULL);
521         HDR_SET(HDR__BCC, NULL);
522         HDR_SET(HDR__TO, NULL);
523     }
524     /* Now make sure everything is there. */
525     for (hp = Table; hp < ARRAY_END(Table); hp++)
526         if (hp->Type == HTreq && hp->Value == NULL) {
527             snprintf(Error, sizeof(Error),
528                      "Required \"%s\" header is missing", hp->Name);
529             return Error;
530         }
531
532     return NULL;
533 }
534
535
536 /*
537 **  See if the user has more included text than new text.  Simple-minded,
538 **  but reasonably effective for catching neophyte's mistakes.  Son-of-1036
539 **  says:
540 **
541 **      NOTE: While encouraging trimming is desirable, the 50% rule imposed
542 **      by some old posting agents is both inadequate and counterproductive.
543 **      Posters do not respond to it by being more selective about quoting;
544 **      they respond by padding short responses, or by using different
545 **      quoting styles to defeat automatic analysis.  The former adds
546 **      unnecessary noise and volume, while the latter also defeats more
547 **      useful forms of automatic analysis that reading agents might wish to
548 **      do.
549 **
550 **      NOTE:  At the very least, if a minimum-unquoted quota is being set,
551 **      article bodies shorter than (say) 20 lines, or perhaps articles
552 **      which exceed the quota by only a few lines, should be exempt.  This
553 **      avoids the ridiculous situation of complaining about a 5-line
554 **      response to a 6-line quote.
555 **
556 **  Accordingly, bodies shorter than 20 lines are exempt.  A line starting
557 **  with >, |, or : is included text.  Decrement the count on lines starting
558 **  with < so that we don't reject diff(1) output.
559 */
560 static const char *
561 CheckIncludedText(const char *p, int lines)
562 {
563     int i;
564
565     if (lines < 20)
566         return NULL;
567     for (i = 0; ; p++) {
568         switch (*p) {
569             case '>': i++; break;
570             case '|': i++; break;
571             case ':': i++; break;
572             case '<': i--; break;
573             default:       break;
574         }
575         p = strchr(p, '\n');
576         if (p == NULL)
577             break;
578     }
579     if (i * 2 > lines)
580         return "Article not posted -- more included text than new text";
581     return NULL;
582 }
583
584 \f
585
586 /*
587 **  Try to mail an article to the moderator of the group.
588 */
589 static const char *
590 MailArticle(char *group, char *article)
591 {
592     static char CANTSEND[] = "Can't send text to mailer";
593     FILE        *F;
594     HEADER      *hp;
595     int         i;
596     char        *address;
597     char        buff[SMBUF];
598     char        *mta;
599
600     /* Try to get the address first. */
601     if ((address = GetModeratorAddress(NULL, NULL, group, PERMaccessconf->moderatormailer)) == NULL) {
602         snprintf(Error, sizeof(Error), "No mailing address for \"%s\" -- %s",
603                  group, "ask your news administrator to fix this");
604         free(group);  
605         return Error;
606     }
607     free(group);
608
609     /* Now build up the command (ignore format/argument mismatch errors,
610      * in case %s isn't in inconf->mta) and send the headers. */
611     if ((mta = innconf->mta) == NULL)
612         return "Can't start mailer - mta not set";
613     snprintf(buff, sizeof(buff), innconf->mta, address);
614     if ((F = popen(buff, "w")) == NULL)
615         return "Can't start mailer";
616     fprintf(F, "To: %s\n", address);
617     if (FLUSH_ERROR(F)) {
618         pclose(F);
619         return CANTSEND;
620     }
621
622     /* Write the headers, a blank line, then the article. */
623     for (hp = Table; hp < ARRAY_END(Table); hp++)
624         if (hp->Value) {
625             if (*hp->Value == ' ' || *hp->Value == '\t')
626                 fprintf(F, "%s:%s\n", hp->Name, hp->Value);
627             else
628                 fprintf(F, "%s: %s\n", hp->Name, hp->Value);
629             if (FLUSH_ERROR(F)) {
630                 pclose(F);
631                 return CANTSEND;
632             }
633         }
634     for (i = 0; i < OtherCount; i++) {
635         fprintf(F, "%s\n", OtherHeaders[i]);
636         if (FLUSH_ERROR(F)) {
637             pclose(F);
638             return CANTSEND;
639         }
640     }
641     fprintf(F, "\n");
642     i = strlen(article);
643     if (fwrite(article, 1, i, F) != (size_t)i)
644         return "Can't send article";
645     if (FLUSH_ERROR(F)) {
646         pclose(F);
647         return CANTSEND;
648     }
649     i = pclose(F);
650     if (i) {
651         snprintf(Error, sizeof(Error), "Mailer exited with status %d -- %s",
652                  i, "Article might not have been mailed");
653         return Error;
654     }
655     return NULL;
656 }
657
658
659 /*
660 **  Check the newsgroups and make sure they're all valid, that none are
661 **  moderated, etc.
662 */
663 static const char *
664 ValidNewsgroups(char *hdr, char **modgroup)
665 {
666     static char         distbuff[SMBUF];
667     char                *groups;
668     char                *p;
669     bool                approved;
670     struct _DDHANDLE    *h;
671     char                *grplist[2];
672     bool                IsNewgroup;
673     bool                FoundOne;
674     int                 flag;
675     bool                hookpresent = false;
676
677 #ifdef DO_PYTHON
678     hookpresent = PY_use_dynamic;
679 #endif /* DO_PYTHON */
680
681     p = HDR(HDR__CONTROL);
682     IsNewgroup = (p && strncmp(p, "newgroup", 8) == 0);
683     groups = xstrdup(hdr);
684     if ((p = strtok(groups, NGSEPS)) == NULL)
685         return "Can't parse newsgroups line";
686     Error[0] = '\0';
687
688     /* Reject all articles with Approved headers unless the user is allowed to
689        add them, even to unmoderated or local groups.  We want to reject them
690        to unmoderated groups in case there's a disagreement of opinion
691        between various sites as to the moderation status. */
692     approved = HDR(HDR__APPROVED) != NULL;
693     if (approved && !PERMaccessconf->allowapproved) {
694         snprintf(Error, sizeof(Error),
695                  "You are not allowed to approve postings");
696     }
697
698     FoundOne = false;
699     h = DDstart((FILE *)NULL, (FILE *)NULL);
700     do {
701         if (innconf->mergetogroups && p[0] == 't' && p[1] == 'o' && p[2] == '.')
702             p = "to";
703         if (!hookpresent && PERMspecified) {
704             grplist[0] = p;
705             grplist[1] = NULL;
706             if (!PERMmatch(PERMpostlist, grplist)) {
707                 snprintf(Error, sizeof(Error),
708                          "You are not allowed to post to %s\r\n", p);
709             }
710         }
711         if (!OVgroupstats(p, NULL, NULL, NULL, &flag))
712             continue;
713         FoundOne = true;
714         DDcheck(h, p);
715         switch (flag) {
716         case NF_FLAG_OK:
717 #ifdef DO_PYTHON
718         if (PY_use_dynamic) {
719             char    *reply;
720
721             /* Authorize user using Python module method dynamic */
722             if (PY_dynamic(PERMuser, p, true, &reply) < 0) {
723                 syslog(L_NOTICE, "PY_dynamic(): authorization skipped due to no Python dynamic method defined.");
724             } else {
725                 if (reply != NULL) {
726                     syslog(L_TRACE, "PY_dynamic() returned a refuse string for user %s at %s who wants to read %s: %s", PERMuser, ClientHost, p, reply);
727                     snprintf(Error, sizeof(Error), "%s\r\n", reply);
728                     free(reply);
729                     break;
730                 }
731             }
732         }
733 #endif /* DO_PYTHON */
734             break;
735         case NF_FLAG_MODERATED:
736             if (!approved && modgroup != NULL && !*modgroup)
737                 *modgroup = xstrdup(p);
738             break;
739         case NF_FLAG_IGNORE:
740         case NF_FLAG_NOLOCAL:
741             if (!PERMaccessconf->locpost)
742                 snprintf(Error, sizeof(Error),
743                          "Postings to \"%s\" are not allowed here.", p);
744             break;
745         case NF_FLAG_EXCLUDED:
746             /* Do NOT return an error. */
747             break;
748         case NF_FLAG_ALIAS:
749             snprintf(Error, sizeof(Error),
750                      "The newsgroup \"%s\" has been renamed.\n", p);
751             break;
752         }
753     } while ((p = strtok((char *)NULL, NGSEPS)) != NULL);
754     free(groups);
755
756     if (!FoundOne && !IsNewgroup)
757         snprintf(Error, sizeof(Error), "No valid newsgroups in \"%s\"",
758                  MaxLength(hdr,hdr));
759     if (Error[0]) {
760         tmpPtr = DDend(h);
761         free(tmpPtr);
762         if (modgroup != NULL && *modgroup != NULL) {
763             free(*modgroup);
764             *modgroup = NULL;
765         }
766         return Error;
767     }
768
769     p = DDend(h);
770     if (HDR(HDR__DISTRIBUTION) == NULL && *p) {
771         strlcpy(distbuff, p, sizeof(distbuff));
772         HDR_SET(HDR__DISTRIBUTION, distbuff);
773     }
774     free(p);
775     return NULL;
776 }
777
778
779 /*
780 **  Send a quit message to the server, eat its reply.
781 */
782 static void
783 SendQuit(FILE *FromServer, FILE *ToServer)
784 {
785     char        buff[NNTP_STRLEN];
786
787     fprintf(ToServer, "quit\r\n");
788     fflush(ToServer);
789     fclose(ToServer);
790     fgets(buff, sizeof buff, FromServer);
791     fclose(FromServer);
792 }
793
794
795 /*
796 **  Offer the article to the server, return its reply.
797 */
798 static int
799 OfferArticle(char *buff, int buffsize, FILE *FromServer, FILE *ToServer)
800 {
801     static char         CANTSEND[] = "Can't send %s to server, %s";
802
803     fprintf(ToServer, "ihave %s\r\n", HDR(HDR__MESSAGEID));
804     if (FLUSH_ERROR(ToServer)
805      || fgets(buff, buffsize, FromServer) == NULL) {
806         snprintf(buff, sizeof(buff), CANTSEND, "IHAVE", strerror(errno));
807         return -1;
808     }
809     return atoi(buff);
810 }
811
812
813 /*
814 **  Spool article to temp file.
815 */
816 static const char *
817 SpoolitTo(char *article, char *err, char *SpoolDir)
818 {
819     static char CANTSPOOL[NNTP_STRLEN+2];
820     HEADER *hp;
821     FILE *F = NULL;
822     int i, fd;
823     char *tmpspool = NULL;
824     char *spoolfile = NULL;
825     char *q;
826
827     /* Initialize the returned error message */
828     snprintf(CANTSPOOL, sizeof(CANTSPOOL),
829              "%s and can't write text to local spool file", err);
830
831     /* Try to write it to the spool dir. */
832     tmpspool = concatpath(SpoolDir, ".XXXXXX");
833     fd = mkstemp(tmpspool);
834     if (fd < 0) {
835         syslog(L_FATAL, "cant create temporary spool file %s %m", tmpspool);
836         goto fail;
837     }
838     F = fdopen(fd, "w");
839     if (F == NULL) {
840         syslog(L_FATAL, "cant open %s %m", tmpspool);
841         goto fail;
842     }
843     fchmod(fileno(F), BATCHFILE_MODE);
844
845     /* Write the headers and a blank line. */
846     for (hp = Table; hp < ARRAY_END(Table); hp++)
847         if (hp->Value) {
848             q = xstrndup(hp->Value, hp->Body - hp->Value + hp->Len);
849             if (*hp->Value == ' ' || *hp->Value == '\t')
850                 fprintf(F, "%s:%s\n", hp->Name, q);
851             else
852                 fprintf(F, "%s: %s\n", hp->Name, q);
853             if (FLUSH_ERROR(F)) {
854                 fclose(F);
855                 free(q);
856                 goto fail;
857             }
858             free(q);
859         }
860     for (i = 0; i < OtherCount; i++) {
861         fprintf(F, "%s\n", OtherHeaders[i]);
862         if (FLUSH_ERROR(F)) {
863             fclose(F);
864             goto fail;
865         }
866     }
867     fprintf(F, "\n");
868
869     /* Write the article body */
870     i = strlen(article);
871     if (fwrite(article, 1, i, F) != (size_t)i) {
872         fclose(F);
873         goto fail;
874     }
875
876     /* Flush and catch any errors */
877     if (fclose(F))
878         goto fail;
879
880     /* Rename the spool file to something rnews will pick up. */
881     spoolfile = concatpath(SpoolDir, "XXXXXX");
882     fd = mkstemp(spoolfile);
883     if (fd < 0) {
884         syslog(L_FATAL, "cant create spool file %s %m", spoolfile);
885         goto fail;
886     }
887     close(fd);
888     if (rename(tmpspool, spoolfile) < 0) {
889         syslog(L_FATAL, "cant rename %s %s %m", tmpspool, spoolfile);
890         goto fail;
891     }
892
893     /* Article has been spooled */
894     free(tmpspool);
895     free(spoolfile);
896     return NULL;
897
898  fail:
899     if (tmpspool != NULL)
900         free(tmpspool);
901     if (spoolfile != NULL)
902         free(spoolfile);
903     return CANTSPOOL;
904 }
905
906 /*
907 **  Spool article to temp file.
908 */
909 static const char *
910 Spoolit(char *article, char *err)
911 {
912     return SpoolitTo(article, err, innconf->pathincoming);
913 }
914  
915 static char *Towire(char *p) {
916     char        *q, *r, *s;
917     int         curlen, len = BIG_BUFFER;
918
919     for (r = p, q = s = xmalloc(len); *r != '\0' ;) {
920         curlen = q - s;
921         if (curlen + 3 > len) {
922             len += BIG_BUFFER;
923             s = xrealloc(s, len);
924             q = s + curlen;
925         }
926         if (*r == '\n') {
927             if (r > p) {
928                 if (*(r - 1) != '\r')
929                     *q++ = '\r';
930             } else {
931                 /* this should not happen */
932                 free(s);
933                 return NULL;
934             }
935         }
936         *q++ = *r++;
937     }
938     curlen = q - s;
939     if (curlen + 1 > len) {
940         len++;
941         s = xrealloc(s, len);
942         q = s + curlen;
943     }
944     *q = '\0';
945     return s;
946 }
947
948 const char *
949 ARTpost(char *article,
950         char *idbuff,
951         bool ihave,
952         bool *permanent)
953 {
954     static char CANTSEND[] = "Can't send %s to server, %s";
955     int         i;
956     char        *p, *q;
957     char        *next;
958     HEADER      *hp;
959     FILE        *ToServer;
960     FILE        *FromServer;
961     char        buff[NNTP_STRLEN + 2], frombuf[SMBUF];
962     char        *modgroup = NULL;
963     const char  *error;
964     char        *TrackID;
965     char        *DirTrackID;
966     FILE        *ftd;
967     char        SDir[255];
968
969     /* Assume errors are permanent, until we discover otherwise */
970     *permanent = true;
971
972     /* Set up the other headers list. */
973     if (OtherHeaders == NULL) {
974         OtherSize = HEADER_DELTA;
975         OtherHeaders = xmalloc(OtherSize * sizeof(char *));
976     }
977
978     /* Basic processing. */
979     OtherCount = 0;
980     for (hp = Table; hp < ARRAY_END(Table); hp++) {
981         hp->Size = strlen(hp->Name);
982         hp->Value = hp->Body = NULL;
983     }
984     if ((article = StripOffHeaders(article)) == NULL)
985         return Error;
986     for (i = 0, p = article; p; i++, p = next + 1)
987         if ((next = strchr(p, '\n')) == NULL)
988             break;
989     if (PERMaccessconf->checkincludedtext) {
990         if ((error = CheckIncludedText(article, i)) != NULL)
991                 return error;
992     }
993     if ((error = ProcessHeaders(i, idbuff, ihave)) != NULL)
994         return error;
995     if (i == 0 && HDR(HDR__CONTROL) == NULL)
996         return "Article is empty";
997
998     if ((error = ValidNewsgroups(HDR(HDR__NEWSGROUPS), &modgroup)) != NULL)
999         return error;
1000     
1001     strlcpy(frombuf, HDR(HDR__FROM), sizeof(frombuf));
1002     for (i = 0, p = frombuf;p < frombuf + sizeof(frombuf);)
1003         if ((p = strchr(p, '\n')) == NULL)
1004             break;
1005         else
1006             *p++ = ' ';
1007     HeaderCleanFrom(frombuf);
1008     p = strchr(frombuf, '@');
1009     if (p) {
1010         strlcpy(frombuf, p+1, sizeof(frombuf));
1011         p = strrchr(frombuf, '.');
1012         if (!p) {
1013             if (modgroup)
1014                 free(modgroup);
1015             return "From: address not in Internet syntax";
1016         }
1017     }
1018     else {
1019         if (modgroup)
1020             free(modgroup);
1021         return "From: address not in Internet syntax";
1022     }
1023     if ((p = HDR(HDR__FOLLOWUPTO)) != NULL
1024      && strcmp(p, "poster") != 0
1025      && (error = ValidNewsgroups(p, (char **)NULL)) != NULL) {
1026         if (modgroup)
1027             free(modgroup);
1028         return error;
1029     }
1030     if ((PERMaccessconf->localmaxartsize > 0) &&
1031                 (strlen(article) > (unsigned)PERMaccessconf->localmaxartsize)) {
1032         snprintf(Error, sizeof(Error),
1033                  "Article is bigger then local limit of %ld bytes\n",
1034                  PERMaccessconf->localmaxartsize);
1035         if (modgroup)
1036             free(modgroup);
1037         return Error;
1038     }
1039
1040 #if defined(DO_PERL)
1041     /* Calls the Perl subroutine for headers management */
1042     p = PERMaccessconf->nnrpdperlfilter ? HandleHeaders(article) : NULL;
1043     if (p != NULL) {
1044         if (idbuff) {
1045             if (modgroup)
1046                 sprintf(idbuff, "(mailed to moderator for %s)", modgroup);
1047             else
1048                 strlcpy(idbuff, HDR(HDR__MESSAGEID), SMBUF);
1049         }
1050         if (strncmp(p, "DROP", 4) == 0) {
1051             syslog(L_NOTICE, "%s post %s", ClientHost, p);
1052             if (modgroup)
1053                 free(modgroup);
1054             return NULL;
1055         }
1056         else if (strncmp(p, "SPOOL", 5) == 0) {
1057             syslog(L_NOTICE, "%s post %s", ClientHost, p);
1058             strlcpy(SDir, innconf->pathincoming, sizeof(SDir));
1059             if (modgroup) {
1060                 free(modgroup);
1061                 strlcat(SDir, "/spam/mod", sizeof(SDir));
1062                 return SpoolitTo(article, p, SDir);
1063             }
1064             else {
1065                 strlcat(SDir, "/spam", sizeof(SDir));
1066                 return SpoolitTo(article, p, SDir);
1067             }
1068         }
1069         else
1070         {
1071             if (modgroup)
1072                 free(modgroup);
1073             return p;
1074         }
1075     }
1076 #endif /* defined(DO_PERL) */
1077
1078     /* handle mailing to moderated groups */
1079
1080     if (modgroup) {
1081         if (idbuff != NULL) {
1082             const char *retstr;
1083             retstr = MailArticle(modgroup, article);
1084             strcpy (idbuff,"(mailed to moderator)") ;
1085             return retstr;
1086         }
1087         return MailArticle(modgroup, article);
1088     }
1089
1090     if (idbuff)
1091         strlcpy(idbuff, HDR(HDR__MESSAGEID), SMBUF);
1092
1093     if (PERMaccessconf->spoolfirst)
1094         return Spoolit(article, Error);
1095
1096     if (Offlinepost)
1097          return Spoolit(article,Error);
1098
1099     /* Open a local connection to the server. */
1100     if (PERMaccessconf->nnrpdposthost != NULL)
1101         i = NNTPconnect(PERMaccessconf->nnrpdposthost, PERMaccessconf->nnrpdpostport,
1102                                         &FromServer, &ToServer, buff);
1103     else {
1104 #if     defined(HAVE_UNIX_DOMAIN_SOCKETS)
1105         i = NNTPlocalopen(&FromServer, &ToServer, buff);
1106 #else
1107         i = NNTPremoteopen(innconf->port, &FromServer,
1108                                         &ToServer, buff);
1109 #endif  /* defined(HAVE_UNIX_DOMAIN_SOCKETS) */
1110     }
1111
1112     /* If we cannot open the connection, initialize the error message and
1113      * attempt to recover from this by spooling it locally */
1114     if (i < 0) {
1115         if (buff[0])
1116             strlcpy(Error, buff, sizeof(Error));
1117         else
1118             snprintf(Error, sizeof(Error), CANTSEND, "connect request",
1119                      strerror(errno));
1120         return Spoolit(article,Error);
1121     }
1122     if (Tracing)
1123         syslog(L_TRACE, "%s post_connect %s",
1124             ClientHost, PERMaccessconf->nnrpdposthost ? PERMaccessconf->nnrpdposthost : "localhost");
1125
1126     /* The code below ignores too many return values for my tastes.  At least
1127      * they are all inside cases that are most likely never going to happen --
1128      * for example, if the server crashes. */
1129
1130     /* Offer article to server. */
1131     i = OfferArticle(buff, (int)sizeof buff, FromServer, ToServer);
1132     if (i == NNTP_AUTH_NEEDED_VAL) {
1133         /* Send authorization. */
1134         if (NNTPsendpassword(PERMaccessconf->nnrpdposthost, FromServer, ToServer) < 0) {
1135             snprintf(Error, sizeof(Error), "Can't authorize with %s",
1136                      PERMaccessconf->nnrpdposthost ? PERMaccessconf->nnrpdposthost : "innd");
1137             return Spoolit(article,Error);
1138         }
1139         i = OfferArticle(buff, (int)sizeof buff, FromServer, ToServer);
1140     }
1141     if (i != NNTP_SENDIT_VAL) {
1142         strlcpy(Error, buff, sizeof(Error));
1143         SendQuit(FromServer, ToServer);
1144         if (i != NNTP_HAVEIT_VAL)
1145             return Spoolit(article, Error);
1146         if (i == NNTP_REJECTIT_VAL || i == NNTP_RESENDIT_VAL) {
1147             *permanent = false;
1148         }
1149         return Error;
1150     }
1151     if (Tracing)
1152         syslog(L_TRACE, "%s post starting", ClientHost);
1153
1154     /* Write the headers and a blank line. */
1155     for (hp = Table; hp < ARRAY_END(Table); hp++)
1156         if (hp->Value) {
1157             q = xstrndup(hp->Value, hp->Body - hp->Value + hp->Len);
1158             if (strchr(q, '\n') != NULL) {
1159                 if ((p = Towire(q)) != NULL) {
1160                     /* there is no white space, if hp->Value and hp->Body is the same */
1161                     if (*hp->Value == ' ' || *hp->Value == '\t')
1162                         fprintf(ToServer, "%s:%s\r\n", hp->Name, p);
1163                     else
1164                         fprintf(ToServer, "%s: %s\r\n", hp->Name, p);
1165                     free(p);
1166                 }
1167             } else {
1168                 /* there is no white space, if hp->Value and hp->Body is the same */
1169                 if (*hp->Value == ' ' || *hp->Value == '\t')
1170                     fprintf(ToServer, "%s:%s\r\n", hp->Name, q);
1171                 else
1172                     fprintf(ToServer, "%s: %s\r\n", hp->Name, q);
1173             }
1174             free(q);
1175         }
1176     for (i = 0; i < OtherCount; i++) {
1177         if (strchr(OtherHeaders[i], '\n') != NULL) {
1178             if ((p = Towire(OtherHeaders[i])) != NULL) {
1179                 fprintf(ToServer, "%s\r\n", p);
1180                 free(p);
1181             }
1182         } else {
1183             fprintf(ToServer, "%s\r\n", OtherHeaders[i]);
1184         }
1185     }
1186     fprintf(ToServer, "\r\n");
1187     if (FLUSH_ERROR(ToServer)) {
1188         snprintf(Error, sizeof(Error), CANTSEND, "headers", strerror(errno));
1189         fclose(FromServer);
1190         fclose(ToServer);
1191         return Spoolit(article, Error);
1192     }
1193
1194     /* Send the article, get the server's reply. */
1195     if (NNTPsendarticle(article, ToServer, true) < 0
1196      || fgets(buff, sizeof buff, FromServer) == NULL) {
1197         snprintf(Error, sizeof(Error), CANTSEND, "article", strerror(errno));
1198         fclose(FromServer);
1199         fclose(ToServer);
1200         return Spoolit(article, Error);
1201     }
1202
1203     /* Did the server want the article? */
1204     if ((i = atoi(buff)) != NNTP_TOOKIT_VAL) {
1205         strlcpy(Error, buff, sizeof(Error));
1206         SendQuit(FromServer, ToServer);
1207         syslog(L_TRACE, "%s server rejects %s from %s", ClientHost, HDR(HDR__MESSAGEID), HDR(HDR__PATH));
1208         if (i != NNTP_REJECTIT_VAL && i != NNTP_HAVEIT_VAL)
1209             return Spoolit(article, Error);
1210         if (i == NNTP_REJECTIT_VAL || i == NNTP_RESENDIT_VAL) {
1211             *permanent = false;
1212         }
1213         return Error;
1214     }
1215
1216     /* Send a quit and close down */
1217     SendQuit(FromServer, ToServer);
1218
1219     /* Tracking */
1220     if (PERMaccessconf->readertrack) {
1221         TrackID = concat(innconf->pathlog, "/trackposts/track.",
1222                          HDR(HDR__MESSAGEID), (char *) 0);
1223         if ((ftd = fopen(TrackID,"w")) == NULL) {
1224             DirTrackID = concatpath(innconf->pathlog, "trackposts");
1225             MakeDirectory(DirTrackID, false);
1226             free(DirTrackID);
1227         }
1228         if (ftd == NULL && (ftd = fopen(TrackID,"w")) == NULL) {
1229             syslog(L_ERROR, "%s (%s) open %s: %m",
1230                 ClientHost, Username, TrackID);
1231             free(TrackID);
1232             return NULL;
1233         }
1234         for (hp = Table; hp < ARRAY_END(Table); hp++)
1235             if (hp->Value) {
1236                 q = xstrndup(hp->Value, hp->Body - hp->Value + hp->Len);
1237                 if (strchr(q, '\n') != NULL) {
1238                     if ((p = Towire(q)) != NULL) {
1239                         /* there is no white space, if hp->Value and hp->Body is the same */
1240                         if (*hp->Value == ' ' || *hp->Value == '\t')
1241                             fprintf(ftd, "%s:%s\r\n", hp->Name, p);
1242                         else
1243                             fprintf(ftd, "%s: %s\r\n", hp->Name, p);
1244                         free(p);
1245                     }
1246                 } else {
1247                     /* there is no white space, if hp->Value and hp->Body is the same */
1248                     if (*hp->Value == ' ' || *hp->Value == '\t')
1249                         fprintf(ftd, "%s:%s\r\n", hp->Name, q);
1250                     else
1251                         fprintf(ftd, "%s: %s\r\n", hp->Name, q);
1252                 }
1253                 free(q);
1254             }
1255         for (i = 0 ; i < OtherCount ; i++) {
1256             if (strchr(OtherHeaders[i], '\n') != NULL) {
1257                 if ((p = Towire(OtherHeaders[i])) != NULL) {
1258                     fprintf(ftd, "%s\r\n", p);
1259                     free(p);
1260                 }
1261             } else {
1262                 fprintf(ftd, "%s\r\n", OtherHeaders[i]);
1263             }
1264         }
1265         fprintf(ftd,"\r\n");
1266         NNTPsendarticle(article, ftd, true);
1267         if (fclose(ftd) != EOF) {
1268             syslog(L_NOTICE, "%s (%s) posttrack ok %s",
1269                 ClientHost, Username, TrackID);
1270             if (LLOGenable)
1271                 fprintf(locallog, "%s (%s) posttrack ok %s\n",
1272                     ClientHost, Username, TrackID);
1273         } else {
1274             syslog(L_ERROR, "%s (%s) posttrack error 2 %s",
1275                 ClientHost, Username, TrackID);
1276         }
1277         free(TrackID);
1278     }
1279
1280     return NULL;
1281 }