Group.C 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  1. //
  2. // Group.C -- Manage newsgroup groups
  3. //
  4. // Copyright 2003-2013 Michael Sweet
  5. // Copyright 2002 Greg Ercolano
  6. //
  7. // This program is free software; you can redistribute it and/or modify
  8. // it under the terms of the GNU General Public Licensse as published by
  9. // the Free Software Foundation; either version 2 of the License, or
  10. // (at your option) any later version.
  11. //
  12. // This program is distributed in the hope that it will be useful,
  13. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. // GNU General Public License for more details.
  16. //
  17. // You should have received a copy of the GNU General Public License
  18. // along with this program; if not, write to the Free Software
  19. // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  20. //
  21. // 80 //////////////////////////////////////////////////////////////////////////
  22. #include "Group.H"
  23. #include <dirent.h>
  24. // RETURN ASCII VERSION OF AN UNSIGNED LONG
  25. static const char *ultoa(unsigned long num)
  26. {
  27. static char s[80];
  28. sprintf(s, "%lu", num);
  29. return(s);
  30. }
  31. const char *Group::Dirname()
  32. {
  33. // CONVERT GROUP NAME TO A DIRECTORY NAME
  34. // eg. rush.general -> /var/spool/news/rush/general
  35. //
  36. dirname = G_conf.SpoolDir();
  37. dirname += "/";
  38. dirname += name;
  39. for ( int t=0; dirname[t]; t++ )
  40. if ( dirname[t] == '.' )
  41. dirname[t] = '/';
  42. return(dirname.c_str());
  43. }
  44. // SAVE OUT GROUP'S ".info" FILE
  45. // Returns -1 on error, errmsg has reason.
  46. //
  47. int Group::SaveInfo(int dolock)
  48. {
  49. string path = Dirname();
  50. path += "/.info";
  51. // WRITE OUT INFO FILE
  52. int ilock = -1;
  53. if ( dolock ) { ilock = WriteLock(); }
  54. {
  55. FILE *fp = fopen(path.c_str(), "w");
  56. if ( fp == NULL )
  57. {
  58. errmsg = path;
  59. errmsg += ": ";
  60. errmsg += strerror(errno);
  61. G_conf.LogMessage(L_ERROR, "Group::SaveInfo(): %s", errmsg.c_str());
  62. if ( dolock ) Unlock(ilock);
  63. return(-1);
  64. }
  65. WriteString(fp, "start ");
  66. WriteString(fp, ultoa(start));
  67. WriteString(fp, "\n");
  68. WriteString(fp, "end ");
  69. WriteString(fp, ultoa(end));
  70. WriteString(fp, "\n");
  71. WriteString(fp, "total ");
  72. WriteString(fp, ultoa(total));
  73. WriteString(fp, "\n");
  74. fflush(fp);
  75. fsync(fileno(fp));
  76. fclose(fp);
  77. }
  78. if ( dolock ) { Unlock(ilock); }
  79. return(0);
  80. }
  81. // BUILD GROUP INFO FROM ACTUAL ARTICLES ON DISK
  82. // Do this if info file doesn't already exist
  83. // Returns -1 on error, errmsg has reason.
  84. //
  85. int Group::BuildInfo(int dolock)
  86. {
  87. int wlock = -1;
  88. int ret = 0;
  89. if ( dolock ) { wlock = WriteLock(); }
  90. start = 0;
  91. end = 0;
  92. total = 0;
  93. DIR *dir;
  94. struct dirent *dent;
  95. struct stat fileinfo;
  96. string filename;
  97. unsigned long temp;
  98. if ((dir = opendir(dirname.c_str())) != NULL)
  99. {
  100. while ((dent = readdir(dir)) != NULL)
  101. {
  102. // Skip entries that don't start with a number
  103. if (!isdigit(dent->d_name[0] & 255)) continue;
  104. filename = dirname + "/" + dent->d_name;
  105. // Using "modulus dirs" for optimization?
  106. if (G_conf.MsgModDirs())
  107. {
  108. // Expect a modulus subdir
  109. if (stat(filename.c_str(), &fileinfo)) continue;
  110. if (!S_ISDIR(fileinfo.st_mode)) continue;
  111. DIR *modulus_dir;
  112. struct dirent *modulus_dent;
  113. struct stat modulus_fileinfo;
  114. string modulus_filename;
  115. // Descend into 'modulus dir'
  116. // /path/fltk/general/1000/1999
  117. // ------------ ---- ----
  118. // | | |
  119. // | | Msg#
  120. // | Modulus dir
  121. // Group name
  122. //
  123. if ((modulus_dir = opendir(filename.c_str())) != NULL)
  124. {
  125. while ((modulus_dent = readdir(modulus_dir)) != NULL)
  126. {
  127. // Skip entries that don't start with a number
  128. if (!isdigit(modulus_dent->d_name[0] & 255)) continue;
  129. modulus_filename = filename + "/" + modulus_dent->d_name;
  130. // Skip directories
  131. if (stat(modulus_filename.c_str(), &modulus_fileinfo)) continue;
  132. if (S_ISDIR(modulus_fileinfo.st_mode)) continue;
  133. // Convert message number to an integer and compare...
  134. temp = strtoul(modulus_dent->d_name, NULL, 10);
  135. if (temp < start || total == 0) start = temp;
  136. if (temp > end || total == 0) end = temp;
  137. total ++;
  138. }
  139. }
  140. closedir(modulus_dir);
  141. }
  142. else
  143. {
  144. // Skip directories
  145. if (stat(filename.c_str(), &fileinfo)) continue;
  146. if (S_ISDIR(fileinfo.st_mode)) continue;
  147. // Convert message number to an integer, cacl start/end/total
  148. temp = strtoul(dent->d_name, NULL, 10);
  149. if (temp < start || total == 0) start = temp;
  150. if (temp > end || total == 0) end = temp;
  151. total ++;
  152. }
  153. }
  154. closedir(dir);
  155. }
  156. ret = SaveInfo(0);
  157. if ( dolock ) { Unlock(wlock); }
  158. return(ret);
  159. }
  160. // LOAD GROUP INFO
  161. // If none exists, create a .info file.
  162. // Returns -1 on error, errmsg has reason.
  163. //
  164. int Group::LoadInfo(int dolock)
  165. {
  166. // LOAD INFO FILE
  167. string path = Dirname();
  168. path += "/.info";
  169. FILE *fp = fopen(path.c_str(), "r");
  170. if ( fp == NULL )
  171. {
  172. // NO INFO FILE? BUILD ONE
  173. if ( errno == ENOENT )
  174. {
  175. // Only build if its a valid group (has a .config file)
  176. if ( ! IsValidGroup() )
  177. {
  178. errmsg = "invalid group";
  179. G_conf.LogMessage(L_ERROR, "LoadInfo(): %s", errmsg.c_str());
  180. return(-1);
  181. }
  182. return(BuildInfo(dolock));
  183. }
  184. errmsg = path;
  185. errmsg += ": ";
  186. errmsg += strerror(errno);
  187. G_conf.LogMessage(L_ERROR, "Group::LoadInfo(): %s", errmsg.c_str());
  188. return(-1);
  189. }
  190. int ilock = -1;
  191. if ( dolock ) { ilock = ReadLock(); }
  192. {
  193. char buf[LINE_LEN];
  194. // char foo[256];
  195. while ( fgets(buf, LINE_LEN-1, fp) )
  196. {
  197. // REMOVE TRAILING \n
  198. if ( strlen(buf) > 1 )
  199. buf[strlen(buf)-1] = 0;
  200. // SKIP BLANK LINES AND COMMENTS
  201. if ( buf[0] == '#' || buf[0] == '\n' ) continue;
  202. if ( sscanf(buf, "start %lu", &start) == 1 )
  203. { continue; }
  204. if ( sscanf(buf, "end %lu", &end ) == 1 )
  205. { continue; }
  206. if ( sscanf(buf, "total %lu", &total) == 1 )
  207. { continue; }
  208. }
  209. fclose(fp);
  210. }
  211. if ( dolock ) { Unlock(ilock); }
  212. return(0);
  213. }
  214. // LOAD GROUP'S ".config" FILE
  215. // Returns -1 on error, errmsg has reason.
  216. //
  217. int Group::LoadConfig(int dolock)
  218. {
  219. // LOAD CONFIG FILE
  220. string path = Dirname();
  221. path += "/.config";
  222. FILE *fp = fopen(path.c_str(), "r");
  223. if ( fp == NULL )
  224. {
  225. errmsg = path;
  226. errmsg += ": ";
  227. errmsg += strerror(errno);
  228. G_conf.LogMessage(L_ERROR, "Group::LoadConfig(): %s", errmsg.c_str());
  229. return(-1);
  230. }
  231. int ilock = -1;
  232. if ( dolock ) { ilock = ReadLock(); }
  233. {
  234. char buf[LINE_LEN];
  235. char foo[256];
  236. ccpost = "";
  237. while ( fgets(buf, LINE_LEN-1, fp) )
  238. {
  239. // REMOVE TRAILING \n
  240. if ( strlen(buf) > 1 )
  241. buf[strlen(buf)-1] = 0;
  242. // SKIP BLANK LINES AND COMMENTS
  243. if ( buf[0] == '#' || buf[0] == '\n' ) continue;
  244. if ( strncmp(buf, "description ", strlen("description ")) == 0 )
  245. {
  246. desc = buf + strlen("description ");
  247. continue;
  248. }
  249. if ( sscanf(buf, "creator %255s", foo) == 1 )
  250. { creator = foo; continue; }
  251. if ( sscanf(buf, "postok %d", &postok) == 1 )
  252. { continue; }
  253. if ( sscanf(buf, "postlimit %d", &postlimit) == 1 )
  254. { continue; }
  255. if ( sscanf(buf, "ccpost %255s", foo) == 1 )
  256. {
  257. // Add trailing comma if none
  258. if ( ccpost != "" && ccpost != "-" )
  259. {
  260. const char *endptr = ccpost.c_str() + ccpost.length() - 1;
  261. if ( *endptr != ',' )
  262. { ccpost += ","; }
  263. }
  264. ccpost += foo;
  265. continue;
  266. }
  267. if ( sscanf(buf, "replyto %255s", foo) == 1 )
  268. { replyto = foo; continue; }
  269. if ( sscanf(buf, "voidemail %255s", foo) == 1 )
  270. { voidemail = foo; continue; }
  271. }
  272. fclose(fp);
  273. }
  274. if ( dolock ) { Unlock(ilock); }
  275. if ( ccpost == "" ) ccpost = "-";
  276. G_conf.LogMessage(L_DEBUG, "ccpost is now '%s' %s", ccpost.c_str(),
  277. errmsg.c_str());
  278. return(0);
  279. }
  280. // SAVE OUT GROUP'S ".config" FILE
  281. // This should only be done by manually invoking 'newsd -newgroup'.
  282. // Returns -1 on error, errmsg has reason.
  283. //
  284. int Group::SaveConfig()
  285. {
  286. string path = Dirname();
  287. path += "/.config";
  288. // WRITE OUT CONFIG FILE
  289. int ilock = WriteLock();
  290. {
  291. FILE *fp = fopen(path.c_str(), "w");
  292. if ( fp == NULL )
  293. {
  294. errmsg = path;
  295. errmsg += ": ";
  296. errmsg += strerror(errno);
  297. Unlock(ilock);
  298. return(-1);
  299. }
  300. WriteString(fp, "description ");
  301. WriteString(fp, desc.c_str());
  302. WriteString(fp, "\n");
  303. WriteString(fp, "creator ");
  304. WriteString(fp, creator.c_str());
  305. WriteString(fp, "\n");
  306. WriteString(fp, "postok ");
  307. WriteString(fp, ultoa((unsigned long)postok));
  308. WriteString(fp, "\n");
  309. WriteString(fp, "postlimit ");
  310. WriteString(fp, ultoa((unsigned long)postlimit));
  311. WriteString(fp, "\n");
  312. WriteString(fp, "ccpost ");
  313. WriteString(fp, ccpost.c_str());
  314. WriteString(fp, "\n");
  315. WriteString(fp, "replyto ");
  316. WriteString(fp, replyto.c_str());
  317. WriteString(fp, "\n");
  318. WriteString(fp, "voidemail ");
  319. WriteString(fp, voidemail.c_str());
  320. WriteString(fp, "\n");
  321. fflush(fp);
  322. fsync(fileno(fp));
  323. fclose(fp);
  324. }
  325. Unlock(ilock);
  326. return(0);
  327. }
  328. // LOAD INFO FOR GROUP
  329. // Set dolock=0 if lock has already been made
  330. // to prevent deadlocks.
  331. //
  332. int Group::Load(const char *group_name, int dolock)
  333. {
  334. valid = 0; // assume failed until success
  335. if ( strlen(group_name) >= GROUP_MAX )
  336. { errmsg = "Group name too long"; return(-1); }
  337. struct stat sbuf;
  338. if ( stat(Dirname(), &sbuf) < 0 )
  339. {
  340. errmsg = "invalid group name '";
  341. errmsg += group_name;
  342. errmsg += "': ";
  343. errmsg += strerror(errno);
  344. G_conf.LogMessage(L_ERROR, "Group::Load(): %s", errmsg.c_str());
  345. return(-1);
  346. }
  347. // GET CREATION TIME FROM DIR'S DATESTAMP
  348. ctime = sbuf.st_ctime;
  349. // LOAD INFO FILE
  350. // Builds one if it doesn't exist
  351. //
  352. name = group_name;
  353. if ( LoadInfo(dolock) < 0 )
  354. { return(-1); }
  355. // LOAD CONFIG FILE
  356. if ( LoadConfig(dolock) < 0 )
  357. { return(-1); }
  358. // GROUP IS NOW VALID
  359. valid = 1;
  360. return(0);
  361. }
  362. // WRITE NULL TERMINATED STRING TO FD
  363. int Group::WriteString(FILE *fp, const char *buf)
  364. {
  365. size_t len = strlen(buf);
  366. if ( fwrite(buf, 1, len, fp) != len)
  367. {
  368. errmsg = "write error";
  369. G_conf.LogMessage(L_ERROR, "Group::WriteString(): %s", errmsg.c_str());
  370. return(-1);
  371. }
  372. return(0);
  373. }
  374. // FIND ARTICLE NUMBER GIVEN A MESSAGEID
  375. int Group::FindArticleByMessageID(const char *message_id, unsigned long &number)
  376. {
  377. char *ptr;
  378. // See if we have a Message-Id of the form number-groupname@servername...
  379. ptr = NULL;
  380. number = strtoul(message_id + 1, &ptr, 10);
  381. if (!ptr || *ptr != '-')
  382. {
  383. // No, do a slow lookup...
  384. int idlen = strlen(message_id);
  385. unsigned long temp;
  386. char line[1024];
  387. FILE *fp;
  388. // Load dirname with the correct group directory...
  389. Dirname();
  390. for (number = 0, temp = Start(); temp <= End(); temp ++)
  391. {
  392. snprintf(line, sizeof(line), "%s/%lu", dirname.c_str(), temp);
  393. if ((fp = fopen(line, "r")) != NULL)
  394. {
  395. while (fgets(line, sizeof(line), fp) != NULL)
  396. // Stop at the first blank line or a Message-ID: header
  397. if (line[0] == '\n' || !strncasecmp(line, "Message-Id:", 11))
  398. break;
  399. fclose(fp);
  400. if (line[0] != '\n')
  401. {
  402. // Compare this message ID against the search ID...
  403. ptr = line + 11;
  404. while (*ptr && isspace(*ptr & 255))
  405. ptr ++;
  406. // TODO: is the message ID case insensitive???
  407. if (!strncmp(ptr, message_id, idlen) && ptr[idlen] == '\n')
  408. {
  409. // Found it!
  410. number = temp;
  411. break;
  412. }
  413. }
  414. }
  415. }
  416. // Return an error if necessary...
  417. if (!number)
  418. {
  419. errmsg = string("Message-ID not found: ") + message_id;
  420. G_conf.LogMessage(L_ERROR, "Message-ID not found: %s", message_id);
  421. return(-1);
  422. }
  423. }
  424. return (0);
  425. }
  426. // READ LOCK
  427. int Group::ReadLock()
  428. {
  429. string lockpath = Dirname();
  430. lockpath += "/.lock";
  431. int fd = open(lockpath.c_str(), O_CREAT|O_WRONLY, 0644);
  432. if ( fd < 0 )
  433. {
  434. errmsg = lockpath;
  435. errmsg += ": ";
  436. errmsg += strerror(errno);
  437. G_conf.LogMessage(L_ERROR, "Group::ReadLock(): %s", errmsg.c_str());
  438. return(-1);
  439. }
  440. if ( flock(fd, LOCK_SH) < 0 )
  441. {
  442. errmsg = "flock(";
  443. errmsg += lockpath;
  444. errmsg += ", SHARED): ";
  445. errmsg += strerror(errno);
  446. G_conf.LogMessage(L_ERROR, "Group::ReadLock(): %s", errmsg.c_str());
  447. return(-1);
  448. }
  449. return(fd);
  450. }
  451. // WRITE LOCK
  452. int Group::WriteLock()
  453. {
  454. string lockpath = Dirname();
  455. lockpath += "/.lock";
  456. int fd = open(lockpath.c_str(), O_CREAT|O_WRONLY, 0644);
  457. if ( fd < 0 )
  458. {
  459. errmsg = lockpath;
  460. errmsg += ": ";
  461. errmsg += strerror(errno);
  462. G_conf.LogMessage(L_ERROR, "Group::WriteLock(): %s", errmsg.c_str());
  463. return(-1);
  464. }
  465. if ( flock(fd, LOCK_EX) < 0 )
  466. {
  467. errmsg = "flock(";
  468. errmsg += lockpath;
  469. errmsg += ", EXCLUSIVE): ";
  470. errmsg += strerror(errno);
  471. G_conf.LogMessage(L_ERROR, "Group::WriteLock(): %s", errmsg.c_str());
  472. return(-1);
  473. }
  474. return(fd);
  475. }
  476. // RELEASE POSTING LOCK
  477. void Group::Unlock(int fd)
  478. {
  479. if ( fd > 0 )
  480. { flock(fd, LOCK_UN); close(fd); }
  481. }
  482. // REORDER AN ARTICLE'S HEADER
  483. // Why is this here? Because POST is a group method,
  484. // so we need it here.
  485. //
  486. void Group::ReorderHeader(const char*overview[], vector<string>& head)
  487. {
  488. // REFORMAT HEADER IN OVERVIEW FORMAT
  489. vector<string> newhead;
  490. // SORT OVERVIEW HEADERS TO TOP
  491. for ( int t=0; overview[t]; t++ )
  492. {
  493. for ( unsigned int r=0; r<head.size(); r++ )
  494. {
  495. if (strncmp(overview[t], head[r].c_str(), strlen(overview[t])) == 0)
  496. {
  497. newhead.push_back(head[r]);
  498. head.erase(head.begin() + r);
  499. break;
  500. }
  501. }
  502. }
  503. // APPEND THE REST
  504. for ( unsigned int r=0; r<head.size(); r++ )
  505. newhead.push_back(head[r]);
  506. // UPDATE HEAD WITH NEW SORT ORDER
  507. head = newhead;
  508. }
  509. // RETURN CURRENT DATE IN RFC822 FORMAT
  510. // eg: Sun, 27 Mar 83 20:39:37 -0800
  511. //
  512. // Modified to use 4 digit year instead of lossy 2 digit.
  513. // This appears to be some kind of modern deviation;
  514. // I see nothing in RFC 822 about this. Maybe a post Y2K mod?
  515. // -erco
  516. //
  517. // 4-digit year format was added in RFC 2822
  518. // - mike
  519. const char *Group::DateRFC822()
  520. {
  521. time_t lt = time(NULL);
  522. struct tm *tm = localtime(&lt);
  523. const char *wday[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
  524. const char *mon[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  525. "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
  526. // spec: Wdy, DD Mon YY HH:MM:SS TZ (where TZ is a GMT offset "<+/->HHMM")
  527. // eg: Sun, 27 Mar 83 20:39:37 -0800
  528. // Sun, 27 Mar 83 20:39:37 +0200
  529. //
  530. char gmtoff_sign = (tm->tm_gmtoff<0) ? '-' : '+';
  531. long gmtoff_abs = (tm->tm_gmtoff<0) ? -(tm->tm_gmtoff) : (tm->tm_gmtoff);
  532. int gmtoff_hour = gmtoff_abs / 3600; // secs -> hours
  533. int gmtoff_min = (gmtoff_abs / 60) % 60; // secs -> mins
  534. sprintf(datebuf, "%.3s, %02d %.3s %d %02d:%02d:%02d %c%02d%02d",
  535. (const char*)wday[tm->tm_wday],
  536. (int)tm->tm_mday,
  537. (const char*)mon[tm->tm_mon],
  538. (int)tm->tm_year + 1900,// 4 digit date instead of 2 digit (mozilla)
  539. (int)tm->tm_hour,
  540. (int)tm->tm_min,
  541. (int)tm->tm_sec,
  542. (char)gmtoff_sign,
  543. (int)gmtoff_hour,
  544. (int)gmtoff_min);
  545. return(datebuf);
  546. }
  547. // POST ARTICLE
  548. int Group::Post(const char *overview[],
  549. vector<string> &head,
  550. vector<string> &body,
  551. const char *remoteip_str,
  552. bool force)
  553. {
  554. // DETERMINE GROUP BEING POSTED TO
  555. int found = 0;
  556. char postgroup[255];
  557. for ( unsigned int t=0; !found && t<head.size(); t++ )
  558. {
  559. if ( strncmp(head[t].c_str(), "Newsgroups: ", strlen("Newsgroups: "))
  560. == 0 )
  561. {
  562. if ( sscanf(head[t].c_str(), "Newsgroups: %254s", postgroup) == 1 )
  563. { found = 1; }
  564. }
  565. }
  566. if ( ! found )
  567. { errmsg = "article has no 'Newsgroups' field"; return(-1); }
  568. // LOCK FOR POSTING
  569. Name(postgroup);
  570. int plock = WriteLock();
  571. {
  572. // LOAD INFO FOR THIS GROUP
  573. if ( Load(postgroup, 0) < 0 )
  574. { errmsg = "no such group"; Unlock(plock); return(-1); }
  575. if ( postok == 0 && !force)
  576. {
  577. errmsg = "posting disabled for group '";
  578. errmsg += postgroup;
  579. errmsg += "'";
  580. G_conf.LogMessage(L_ERROR, "Group::Post(): %s", errmsg.c_str());
  581. Unlock(plock);
  582. return(-1);
  583. }
  584. if (*G_conf.SpamFilter())
  585. {
  586. // Run spam filter to see if this is spam before we post...
  587. FILE *p; // Pipe stream
  588. int status; // Exit status
  589. char command[1024]; // Command to run
  590. snprintf(command, sizeof(command), "%s >/dev/null 2>/dev/null",
  591. G_conf.SpamFilter());
  592. if ((p = popen(command, "w")) == NULL)
  593. {
  594. errmsg = "spam filter command failed to execute";
  595. Unlock(plock);
  596. return(-1);
  597. }
  598. // Send the message to the filter...
  599. for ( unsigned int t=0; t<head.size(); t++ )
  600. fprintf(p, "%s\n", head[t].c_str());
  601. fputs("\n", p);
  602. for ( unsigned int t=0; t<body.size(); t++ )
  603. fprintf(p, "%s\n", body[t].c_str());
  604. // Close the pipe to the command and get the exit status...
  605. status = pclose(p);
  606. if (status)
  607. {
  608. errmsg = "spam filter rejected message";
  609. Unlock(plock);
  610. return(-1);
  611. }
  612. }
  613. // OPEN NEW ARTICLE
  614. unsigned long msgnum = 0;
  615. int fd;
  616. for ( msgnum=End() + 1; 1; msgnum++ )
  617. {
  618. // Build path to article
  619. string path;
  620. // Using modulus dirs?
  621. if ( G_conf.MsgModDirs() )
  622. {
  623. path = Dirname(); // "/path/fltk/general"
  624. path += "/"; // "/path/fltk/general/"
  625. path += ultoa((msgnum/1000)*1000); // "/path/fltk/general/1000"
  626. // See if modulus directory exists -- if not, create
  627. struct stat sbuf;
  628. if ( stat(path.c_str(), &sbuf) < 0 )
  629. {
  630. if ( mkdir(path.c_str(), 0777) )
  631. {
  632. errmsg = "can't create modulus dir: mkdir(";
  633. errmsg += path;
  634. errmsg += ",0777): ";
  635. errmsg += strerror(errno);
  636. G_conf.LogMessage(L_ERROR, "Group::Post(): ", errmsg.c_str());
  637. Unlock(plock);
  638. return(-1);
  639. }
  640. }
  641. else if (!S_ISDIR(sbuf.st_mode))
  642. {
  643. errmsg = path;
  644. errmsg += " is not a directory (expected a modulus dir)";
  645. G_conf.LogMessage(L_ERROR, "Group::Post(): ", errmsg.c_str());
  646. Unlock(plock);
  647. return(-1);
  648. }
  649. path += "/"; // "/path/fltk/general/1000/"
  650. path += ultoa(msgnum); // "/path/fltk/general/1000/1999"
  651. }
  652. else
  653. {
  654. path = Dirname(); // "/path/fltk/general"
  655. path += "/"; // "/path/fltk/general/"
  656. path += ultoa(msgnum); // "/path/fltk/general/1999"
  657. }
  658. if ((fd = open(path.c_str(), O_CREAT|O_EXCL|O_WRONLY, 0644)) == -1)
  659. {
  660. if ( errno == EEXIST )
  661. { continue; } // try next article number
  662. errmsg = path;
  663. errmsg += ": ";
  664. errmsg += strerror(errno);
  665. G_conf.LogMessage(L_ERROR, "Group::Post(): ", errmsg.c_str());
  666. Unlock(plock);
  667. return(-1);
  668. }
  669. break;
  670. }
  671. // STRIP UNWANTED INFO FROM HEADER
  672. // HEADER NAMES ARE CASE INSENSITIVE: INTERNET DRAFT (Son of RFC1036)
  673. for ( unsigned int t=0; t<head.size(); t++ )
  674. {
  675. if ( !strncasecmp(head[t].c_str(), "Lines: ", 7) ||
  676. !strncasecmp(head[t].c_str(), "Message-ID: ", 12) ||
  677. !strncasecmp(head[t].c_str(), "Date: ", 6) ||
  678. !strncasecmp(head[t].c_str(), "NNTP-Posting-Host: ", 19) )
  679. {
  680. head.erase( head.begin() + t);
  681. --t;
  682. }
  683. }
  684. // HEADERS ADDED BY NEWS SERVER
  685. char misc[LINE_LEN];
  686. sprintf(misc, "Xref: %s %s:%lu",
  687. (const char*)G_conf.ServerName(),
  688. (const char*)postgroup,
  689. (unsigned long)msgnum);
  690. head.push_back(misc);
  691. sprintf(misc, "Date: %s",
  692. (const char*)DateRFC822());
  693. head.push_back(misc);
  694. sprintf(misc, "NNTP-Posting-Host: %s",
  695. (const char*)remoteip_str);
  696. head.push_back(misc);
  697. sprintf(misc, "Message-ID: <%lu-%s@%s>",
  698. (unsigned long)msgnum,
  699. (const char*)postgroup,
  700. (const char*)G_conf.ServerName());
  701. head.push_back(misc);
  702. sprintf(misc, "Lines: %u",
  703. (unsigned int)body.size());
  704. head.push_back(misc);
  705. ReorderHeader(overview, head);
  706. // WRITE HEADER
  707. for ( unsigned int t=0; t<head.size(); t++ )
  708. {
  709. write(fd, head[t].c_str(), head[t].length());
  710. write(fd, "\n", 1);
  711. }
  712. // WRITE SEPARATOR
  713. write(fd, "\n", 1);
  714. // WRITE BODY
  715. for ( unsigned int t=0; t<body.size(); t++ )
  716. {
  717. write(fd, body[t].c_str(), body[t].length());
  718. write(fd, "\n", 1);
  719. }
  720. // FIRST MSG? START AT 1
  721. if ( Total() == 0 && Start() == 0 )
  722. Start(1);
  723. // THIS IS NEW HIGHEST ARTICLE
  724. End(msgnum);
  725. Total(Total()+1);
  726. SaveInfo(0);
  727. }
  728. Unlock(plock);
  729. return(0);
  730. }
  731. // INTERACTIVELY PROMPT FOR NEW GROUP
  732. // Steps on group's internal variables.
  733. // It's advised parent created a throw-away instance.
  734. //
  735. int Group::NewGroup()
  736. {
  737. int i;
  738. char s[256], in[256];
  739. fprintf(stderr, "Enter the new group's name (eg. 'electronics.ttl'):\n");
  740. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  741. if ( sscanf(in, "%255s", s) != 1 ) return(1);
  742. name = s;
  743. struct stat buf;
  744. if ( stat(Dirname(), &buf) < 0 )
  745. {
  746. // NEWSGROUP DIR DOESNT EXIST, CREATE IT
  747. string cmd = "mkdir -m 0755 -p ";
  748. cmd += Dirname();
  749. G_conf.LogMessage(L_DEBUG, "Executing: %s", cmd.c_str());
  750. if ( system(cmd.c_str()) )
  751. { return(1); }
  752. G_conf.LogMessage(L_ERROR, "OK");
  753. }
  754. fprintf(stderr, "\nIs posting to this group allowed? (Y/n):\n");
  755. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  756. postok = ( in[0] == 'n' || in[0] == 'N' ) ? 0 : 1;
  757. if ( postok )
  758. {
  759. fprintf(stderr,
  760. "\nMaximum #lines for postings, '0' if no max (default=1000):\n");
  761. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  762. if ( sscanf(in, "%d", &i) != 1 ) i = 1000;
  763. postlimit = i;
  764. }
  765. else
  766. { postlimit = 0; }
  767. fprintf(stderr, "\nDescription of newsgroup in one short line, '-' if none "
  768. "(eg. 'Discussion of TTL electronics'):\n");
  769. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  770. in[strlen(in)-1] = 0;
  771. desc = in;
  772. fprintf(stderr, "\nAdministrator's email address for group, '-' if none "
  773. "(default='-'):\n");
  774. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  775. if ( sscanf(in, "%255s", s) != 1 ) strcpy(s, "-");
  776. creator = s;
  777. fprintf(stderr, "\nBCC all postings to these email address(es), "
  778. "'-' if none (default='-')\n");
  779. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  780. if ( sscanf(in, "%255s", s) != 1 ) strcpy(s, "-");
  781. ccpost = s;
  782. if ( ccpost != "-" )
  783. {
  784. fprintf(stderr,
  785. "\nReply-To address for all emails, '-' if none (default='-')\n");
  786. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  787. if ( sscanf(in, "%255s", s) != 1 ) strcpy(s, "-");
  788. replyto = s;
  789. fprintf(stderr,
  790. "\nVoid email address, 'root' if none (default='root')\n");
  791. if ( fgets(in, sizeof(in)-1, stdin) == NULL ) return(1);
  792. if ( sscanf(in, "%255s", s) != 1 ) strcpy(s, "root");
  793. voidemail = s;
  794. }
  795. if ( SaveConfig() < 0 )
  796. {
  797. G_conf.LogMessage(L_ERROR, "ERROR: %s", Errmsg());
  798. return(1);
  799. }
  800. // CREATE AN INFO FILE, SO IT SHOWS UP IN 'LIST'
  801. // This lets the admin be able to subscribe to the group
  802. // and post the first test msg to it.
  803. //
  804. if ( BuildInfo(1) < 0 )
  805. {
  806. G_conf.LogMessage(L_ERROR, "ERROR: %s", Errmsg());
  807. return(1);
  808. }
  809. fprintf(stderr, "\n"
  810. "----------\n"
  811. "--- OK ---\n"
  812. "----------\n"
  813. "\n"
  814. " o New group %s was created.\n"
  815. "\n"
  816. " o Use your news reader to post some test messages.\n"
  817. "\n"
  818. " o You can edit %s/.config later\n"
  819. " to make changes.\n",
  820. (const char*)Name(),
  821. (const char*)Dirname());
  822. return(0);
  823. }
  824. // PARSE MESSAGE FOR HEADERS AND BODY
  825. // On error, returns -1, errmsg contains reason.
  826. //
  827. int Group::ParseArticle(string &msg, vector<string>&head, vector<string>&body)
  828. {
  829. errmsg = "";
  830. // REQUIRED
  831. // --------
  832. // From: <who@bar.com>
  833. // Date: Wdy, DD Mon YY HH:MM:SS TIMEZONE (or: Wdy Mon DD HH:MM:SS YYYY)
  834. // Newsgroups: news.group[,news.group] -- ignore invalid groups
  835. // Subject: xx yy zz
  836. // Message-ID: <unique@host.domain.com>
  837. // Path: <recenthost>,<oldhost>,<olderhost>
  838. //
  839. // OPTIONAL
  840. // --------
  841. // Control: <cmd [arg [arg..]]>
  842. // Distribution: <state abbrev[,abbrev]>
  843. // Organization: <misc text>
  844. // References: <message-id-of-original-message>
  845. // Lines: <#lines in body>
  846. // Xref: <thishost> <newsgroup:article#> [newsgroup:article#]
  847. //
  848. // CONTROL MESSAGES
  849. // ----------------
  850. // 1) Subject: cmsg <cmd>
  851. //
  852. // 2) Control: <cmd>
  853. //
  854. // 3) Newsgroups: all.all.ctl
  855. // Subject: <cmd>
  856. //
  857. char *ss = (char*)msg.c_str(),
  858. *startptr = ss;
  859. int headflag = 1;
  860. // PARSE OUT HEADERS AND BODY
  861. while ( 1 )
  862. {
  863. if ( *ss == 0 )
  864. {
  865. break; // parsed entire msg
  866. }
  867. if ( *ss == '\r' ) // end of line?
  868. {
  869. // Special case: multi-line header?
  870. //
  871. // Load lines of multiline header into single string,
  872. // preserving the \r\n's within the line.
  873. // *Don't* unfold, to preserve line length enforced by browser.
  874. //
  875. // in: "head1: abc\r\n<space>def\r\nhead2: 123\r\n"
  876. // out: head[0] = "head1: abc\r\n def";
  877. // head[1] = "head2: 123";
  878. //
  879. if ( headflag &&
  880. (*ss == '\r' && *(ss+1) == '\n' &&
  881. ( *(ss+2) == ' ' || *(ss+2) == '\t' ) ) )
  882. {
  883. ss += 2;
  884. continue;
  885. }
  886. *ss = 0; // terminate
  887. if ( strcmp(startptr, ".") != 0 )
  888. {
  889. if ( headflag )
  890. { head.push_back(startptr); } // append
  891. else
  892. { body.push_back(startptr); }
  893. }
  894. *ss = '\r';
  895. ++ss;
  896. if ( *ss == '\n' ) { ++ss; } // skip over CRLF
  897. startptr = ss; // save next line start
  898. // CHECK FOR BLANK LINE ENDING HEADER
  899. if ( headflag )
  900. {
  901. if ( *ss == '\r' ) // next line blank?
  902. {
  903. headflag = 0; // disable header mode
  904. ss++; // skip over \r
  905. if ( *ss == '\n' ) { ss++; } // and \n if any
  906. startptr = ss; // start of body
  907. }
  908. }
  909. continue;
  910. }
  911. ++ss;
  912. }
  913. return(0);
  914. }
  915. // UPDATE THE "Path:" HEADER
  916. void Group::UpdatePath(vector<string>&header)
  917. {
  918. const char *pathstr = "Path:";
  919. int pathlen = strlen(pathstr);
  920. string hostname = G_conf.ServerName();
  921. // ADD TO EXISTING PATH (IF EXISTS)
  922. int found = 0;
  923. for ( uint t=0; t<header.size(); t++ )
  924. {
  925. if ( strncasecmp(header[t].c_str(), pathstr, pathlen) == 0 )
  926. {
  927. string info = hostname + ", ";
  928. if ( header[t].c_str()[5] != ' ' )
  929. {
  930. // "Path:lasthost.." -> "Path: lasthost.."
  931. header[t].insert(5," ");
  932. }
  933. // "Path: lasthost.." -> "Path: thishost, lasthost.."
  934. header[t].insert(6, info);
  935. found = 1;
  936. }
  937. }
  938. // CREATE NEW PATH (IF NONE EXISTS)
  939. if ( ! found )
  940. {
  941. string newpath = "Path: " + hostname;
  942. header.push_back(newpath);
  943. }
  944. }
  945. // IS THIS A VALID GROUP?
  946. // Check for the existence of a .config file.
  947. //
  948. int Group::IsValidGroup()
  949. {
  950. // Is this a valid group?
  951. string cfgpath = Dirname();
  952. cfgpath += "/.config";
  953. struct stat sbuf;
  954. if ( stat(cfgpath.c_str(), &sbuf) < 0 ) return(0);
  955. return(1);
  956. }