Release version 997 generated by do-release.sh
[citadel.git] / textclient / tuiconfig.c
1 // Configuration screens that are part of the text mode client.
2 //
3 // Copyright (c) 1987-2022 by the citadel.org team
4 //
5 // This program is open source software.  Use, duplication, and/or
6 // disclosure is subject to the GNU General Purpose License version 3.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12
13 #include "textclient.h"
14
15 extern char temp[];
16 extern char tempdir[];
17 extern char *axdefs[8];
18 extern long highest_msg_read;
19 extern long maxmsgnum;
20 extern unsigned room_flags;
21 extern int screenwidth;
22 char editor_path[PATH_MAX];
23
24
25 // General system configuration command
26 void do_system_configuration(CtdlIPC * ipc) {
27         char buf[256];
28         char sc[NUM_CONFIGS][256];
29         char *resp = NULL;
30         struct ExpirePolicy *site_expirepolicy = NULL;
31         struct ExpirePolicy *mbx_expirepolicy = NULL;
32         int a;
33         int logpages = 0;
34         int r;                  // IPC response code
35         int server_configs = 0;
36
37         // Clear out the config buffers
38         memset(&sc[0][0], 0, sizeof(sc));
39
40         // Fetch the current config
41         r = CtdlIPCGetSystemConfig(ipc, &resp, buf);
42         if (r / 100 == 1) {
43                 server_configs = num_tokens(resp, '\n');
44                 for (a = 0; a < server_configs; ++a) {
45                         if (a < NUM_CONFIGS) {
46                                 extract_token(&sc[a][0], resp, a, '\n', sizeof sc[a]);
47                         }
48                 }
49         }
50         if (resp) {
51                 free(resp);
52         }
53         resp = NULL;
54
55         // Fetch the expire policy (this will silently fail on old servers, resulting in "default" policy)
56         r = CtdlIPCGetMessageExpirationPolicy(ipc, 2, &site_expirepolicy, buf);
57         r = CtdlIPCGetMessageExpirationPolicy(ipc, 3, &mbx_expirepolicy, buf);
58
59         // Identification parameters
60         strprompt("Node name", &sc[0][0], 15);
61         strprompt("Fully qualified domain name", &sc[1][0], 63);
62         strprompt("Human readable node name", &sc[2][0], 20);
63         strprompt("Telephone number", &sc[3][0], 15);
64         strprompt("Geographic location of this system", &sc[12][0], 31);
65         strprompt("Name of system administrator", &sc[13][0], 25);
66         strprompt("Paginator prompt", &sc[10][0], 79);
67
68         // Security parameters
69         snprintf(sc[7], sizeof sc[7], "%d", (boolprompt("Require registration for new users", atoi(&sc[7][0]))));
70         snprintf(sc[29], sizeof sc[29], "%d", (boolprompt("Disable self-service user account creation", atoi(&sc[29][0]))));
71         strprompt("Initial access level for new users", &sc[6][0], 1);
72         strprompt("Access level required to create rooms", &sc[19][0], 1);
73         snprintf(sc[67], sizeof sc[67], "%d", (boolprompt("Allow anonymous guest logins", atoi(&sc[67][0]))));
74         snprintf(sc[4], sizeof sc[4], "%d",
75                  (boolprompt("Automatically give room admin privs to a user who creates a private room", atoi(&sc[4][0]))));
76         snprintf(sc[8], sizeof sc[8], "%d", (boolprompt("Automatically move problem user messages to twit room", atoi(&sc[8][0]))));
77         strprompt("Name of twit room", &sc[9][0], ROOMNAMELEN);
78         snprintf(sc[11], sizeof sc[11], "%d",
79                  (boolprompt("Restrict Internet mail to only those with that privilege", atoi(&sc[11][0]))));
80         snprintf(sc[26], sizeof sc[26], "%d", (boolprompt("Allow admins to Zap (forget) rooms", atoi(&sc[26][0]))));
81
82         if (!IsEmptyStr(&sc[18][0])) {
83                 logpages = 1;
84         }
85         else {
86                 logpages = 0;
87         }
88         logpages = boolprompt("Log all instant messages", logpages);
89         if (logpages) {
90                 strprompt("Name of logging room", &sc[18][0], ROOMNAMELEN);
91         }
92         else {
93                 sc[18][0] = 0;
94         }
95
96         // Server tuning
97         strprompt("Server connection idle timeout (in seconds)", &sc[5][0], 4);
98         strprompt("Maximum concurrent sessions", &sc[14][0], 4);
99         strprompt("Maximum message length", &sc[20][0], 20);
100         strprompt("Minimum number of worker threads", &sc[21][0], 3);
101         strprompt("Maximum number of worker threads", &sc[22][0], 3);
102         snprintf(sc[43], sizeof sc[43], "%d", (boolprompt("Automatically delete committed database logs", atoi(&sc[43][0]))));
103         strprompt("Server IP address (* for 'any')", &sc[37][0], 15);
104         strprompt("POP3 server port (-1 to disable)", &sc[23][0], 5);
105         strprompt("POP3S server port (-1 to disable)", &sc[40][0], 5);
106         strprompt("IMAP server port (-1 to disable)", &sc[27][0], 5);
107         strprompt("IMAPS server port (-1 to disable)", &sc[39][0], 5);
108         strprompt("SMTP MTA server port (-1 to disable)", &sc[24][0], 5);
109         strprompt("SMTP MSA server port (-1 to disable)", &sc[38][0], 5);
110         strprompt("SMTPS server port (-1 to disable)", &sc[41][0], 5);
111         snprintf(sc[72], sizeof sc[72], "%d", (boolprompt("Advertise STARTTLS on the SMTP port", atoi(&sc[72][0]))));
112         strprompt("NNTP server port (-1 to disable)", &sc[70][0], 5);
113         strprompt("NNTPS server port (-1 to disable)", &sc[71][0], 5);
114         strprompt("Postfix TCP Dictionary Port server port (-1 to disable)", &sc[50][0], 5);
115         strprompt("ManageSieve server port (-1 to disable)", &sc[51][0], 5);
116         strprompt("XMPP (Jabber) client to server port (-1 to disable)", &sc[62][0], 5);
117         // strprompt("XMPP (Jabber) server to server port (-1 to disable)", &sc[63][0], 5);   This is just a placeholder.
118
119         // This logic flips the question around, because it's one of those situations where 0=yes and 1=no
120         a = atoi(sc[25]);
121         a = (a ? 0 : 1);
122         a = boolprompt("Correct forged From: lines during authenticated SMTP", a);
123         a = (a ? 0 : 1);
124         snprintf(sc[25], sizeof sc[25], "%d", a);
125
126         snprintf(sc[66], sizeof sc[66], "%d", (boolprompt("Flag messages as spam instead of rejecting", atoi(&sc[66][0]))));
127
128         // This logic flips the question around, because it's one of those situations where 0=yes and 1=no
129         a = atoi(sc[61]);
130         a = (a ? 0 : 1);
131         a = boolprompt("Force IMAP posts in public rooms to be from the user who submitted them", a);
132         a = (a ? 0 : 1);
133         snprintf(sc[61], sizeof sc[61], "%d", a);
134
135         snprintf(sc[45], sizeof sc[45], "%d",
136                  (boolprompt("Allow unauthenticated SMTP clients to spoof my domains", atoi(&sc[45][0]))));
137         snprintf(sc[57], sizeof sc[57], "%d",
138                  (boolprompt("Perform RBL checks at greeting instead of after RCPT", atoi(&sc[57][0]))));
139
140         // LDAP settings
141         if (ipc->ServInfo.supports_ldap) {
142                 a = strlen(&sc[32][0]);
143                 a = (a ? 1 : 0);        // Set only to 1 or 0
144                 a = boolprompt("Do you want to configure LDAP authentication?", a);
145                 if (a) {
146                         strprompt("Host name of LDAP server", &sc[32][0], 127);
147                         strprompt("Port number of LDAP service", &sc[33][0], 5);
148                         strprompt("Base DN", &sc[34][0], 255);
149                         strprompt("Bind DN (or blank for anonymous bind)", &sc[35][0], 255);
150                         strprompt("Password for bind DN (or blank for anonymous bind)", &sc[36][0], 255);
151                 }
152                 else {
153                         strcpy(&sc[32][0], "");
154                 }
155         }
156
157         // Expiry settings
158         strprompt("Default user purge time (days)", &sc[16][0], 5);
159         strprompt("Default room purge time (days)", &sc[17][0], 5);
160
161         // Angels and demons dancing in my head...
162         do {
163                 snprintf(buf, sizeof buf, "%d", site_expirepolicy->expire_mode);
164                 strprompt("System default message expire policy (? for list)", buf, 1);
165                 if (buf[0] == '?') {
166                         scr_printf("\n"
167                                    "1. Never automatically expire messages\n"
168                                    "2. Expire by message count\n" "3. Expire by message age\n");
169                 }
170         } while ((buf[0] < '1') || (buf[0] > '3'));
171         site_expirepolicy->expire_mode = buf[0] - '0';
172
173         // ...lunatics and monsters underneath my bed
174         if (site_expirepolicy->expire_mode == 2) {
175                 snprintf(buf, sizeof buf, "%d", site_expirepolicy->expire_value);
176                 strprompt("Keep how many messages online?", buf, 10);
177                 site_expirepolicy->expire_value = atol(buf);
178         }
179         if (site_expirepolicy->expire_mode == 3) {
180                 snprintf(buf, sizeof buf, "%d", site_expirepolicy->expire_value);
181                 strprompt("Keep messages for how many days?", buf, 10);
182                 site_expirepolicy->expire_value = atol(buf);
183         }
184
185         // Media messiahs preying on my fears...
186         do {
187                 snprintf(buf, sizeof buf, "%d", mbx_expirepolicy->expire_mode);
188                 strprompt("Mailbox default message expire policy (? for list)", buf, 1);
189                 if (buf[0] == '?') {
190                         scr_printf("\n"
191                                    "0. Go with the system default\n"
192                                    "1. Never automatically expire messages\n"
193                                    "2. Expire by message count\n" "3. Expire by message age\n");
194                 }
195         } while ((buf[0] < '0') || (buf[0] > '3'));
196         mbx_expirepolicy->expire_mode = buf[0] - '0';
197
198         // ...Pop culture prophets playing in my ears
199         if (mbx_expirepolicy->expire_mode == 2) {
200                 snprintf(buf, sizeof buf, "%d", mbx_expirepolicy->expire_value);
201                 strprompt("Keep how many messages online?", buf, 10);
202                 mbx_expirepolicy->expire_value = atol(buf);
203         }
204         if (mbx_expirepolicy->expire_mode == 3) {
205                 snprintf(buf, sizeof buf, "%d", mbx_expirepolicy->expire_value);
206                 strprompt("Keep messages for how many days?", buf, 10);
207                 mbx_expirepolicy->expire_value = atol(buf);
208         }
209
210         strprompt("How often to run network jobs (in seconds)", &sc[28][0], 5);
211         strprompt("Default frequency to run POP3 collection (in seconds)", &sc[64][0], 5);
212         strprompt("Fastest frequency to run POP3 collection (in seconds)", &sc[65][0], 5);
213         strprompt("Hour to run purges (0-23)", &sc[31][0], 2);
214         snprintf(sc[42], sizeof sc[42], "%d",
215                  (boolprompt("Enable full text search index (warning: resource intensive)", atoi(&sc[42][0]))));
216         snprintf(sc[46], sizeof sc[46], "%d", (boolprompt("Perform journaling of email messages", atoi(&sc[46][0]))));
217         snprintf(sc[47], sizeof sc[47], "%d", (boolprompt("Perform journaling of non-email messages", atoi(&sc[47][0]))));
218         if ((atoi(&sc[46][0])) || (atoi(&sc[47][0]))) {
219                 strprompt("Email destination of journalized messages", &sc[48][0], 127);
220         }
221
222         // No more Funambol
223         sc[53][0] = 0;
224         sc[54][0] = 0;
225         sc[55][0] = 0;
226         sc[56][0] = 0;
227
228         // External pager stuff
229         int yes_pager = 0;
230         if (strlen(sc[60]) > 0) {
231                 yes_pager = 1;
232         }
233         yes_pager = boolprompt("Configure an external pager tool", yes_pager);
234         if (yes_pager) {
235                 strprompt("External pager tool", &sc[60][0], 255);
236         }
237         else {
238                 sc[60][0] = 0;
239         }
240
241         // Save it
242         scr_printf("Save this configuration? ");
243         if (yesno()) {
244                 r = 1;
245                 for (a = 0; a < NUM_CONFIGS; a++) {
246                         r += 1 + strlen(sc[a]);
247                 }
248                 resp = (char *) calloc(1, r);
249                 if (!resp) {
250                         scr_printf("Can't save config - out of memory!\n");
251                         logoff(ipc, 1);
252                 }
253                 for (a = 0; a < NUM_CONFIGS; a++) {
254                         strcat(resp, sc[a]);
255                         strcat(resp, "\n");
256                 }
257                 r = CtdlIPCSetSystemConfig(ipc, resp, buf);
258                 if (r / 100 != 4) {
259                         scr_printf("%s\n", buf);
260                 }
261                 free(resp);
262
263                 r = CtdlIPCSetMessageExpirationPolicy(ipc, 2, site_expirepolicy, buf);
264                 if (r / 100 != 2) {
265                         scr_printf("%s\n", buf);
266                 }
267
268                 r = CtdlIPCSetMessageExpirationPolicy(ipc, 3, mbx_expirepolicy, buf);
269                 if (r / 100 != 2) {
270                         scr_printf("%s\n", buf);
271                 }
272
273         }
274         if (site_expirepolicy)
275                 free(site_expirepolicy);
276         if (mbx_expirepolicy)
277                 free(mbx_expirepolicy);
278 }
279
280
281 // support function for do_internet_configuration()
282 void get_inet_rec_type(CtdlIPC * ipc, char *buf) {
283         int sel;
284
285         keyopt(" <1> localhost      (Alias for this computer)\n");
286         keyopt(" <2> smart host     (Forward all outbound mail to this host)\n");
287         keyopt(" <3> fallback host  (Send mail to this host only if direct delivery fails)\n");
288         keyopt(" <4> SpamAssassin   (Address of SpamAssassin server)\n");
289         keyopt(" <5> RBL            (domain suffix of spam hunting RBL)\n");
290         keyopt(" <6> masq domains   (Domains as which users are allowed to masquerade)\n");
291         keyopt(" <7> ClamAV         (Address of ClamAV clamd server)\n");
292         sel = intprompt("Which one", 1, 1, 8);
293         switch (sel) {
294         case 1:
295                 strcpy(buf, "localhost");
296                 return;
297         case 2:
298                 strcpy(buf, "smarthost");
299                 return;
300         case 3:
301                 strcpy(buf, "fallbackhost");
302                 return;
303         case 4:
304                 strcpy(buf, "spamassassin");
305                 return;
306         case 5:
307                 strcpy(buf, "rbl");
308                 return;
309         case 6:
310                 strcpy(buf, "masqdomain");
311                 return;
312         case 7:
313                 strcpy(buf, "clamav");
314                 return;
315         }
316 }
317
318
319 // Internet mail configuration
320 void do_internet_configuration(CtdlIPC * ipc) {
321         char buf[256];
322         char *resp = NULL;
323         int num_recs = 0;
324         char **recs = NULL;
325         char ch;
326         int i, j;
327         int quitting = 0;
328         int modified = 0;
329         int r;
330
331         r = CtdlIPCGetSystemConfigByType(ipc, INTERNETCFG, &resp, buf);
332         if (r / 100 == 1) {
333                 while (!IsEmptyStr(resp)) {
334                         extract_token(buf, resp, 0, '\n', sizeof buf);
335                         remove_token(resp, 0, '\n');
336
337                         // "directory" is no longer used.  replace it with "localhost"
338                         char *d = strstr(buf, "|directory");
339                         if (d != NULL) {
340                                 strcpy(d, "|localhost");
341                         }
342
343                         ++num_recs;
344                         if (num_recs == 1)
345                                 recs = malloc(sizeof(char *));
346                         else
347                                 recs = realloc(recs, (sizeof(char *)) * num_recs);
348                         recs[num_recs - 1] = malloc(strlen(buf) + 1);
349                         strcpy(recs[num_recs - 1], buf);
350                 }
351         }
352         if (resp) {
353                 free(resp);
354         }
355
356         do {
357                 scr_printf("\n");
358                 color(BRIGHT_WHITE);
359                 scr_printf("###                    Host or domain                     Record type      \n");
360                 color(DIM_WHITE);
361                 scr_printf("--- -------------------------------------------------- --------------------\n");
362                 for (i = 0; i < num_recs; ++i) {
363                         color(DIM_WHITE);
364                         scr_printf("%3d ", i + 1);
365                         extract_token(buf, recs[i], 0, '|', sizeof buf);
366                         color(BRIGHT_CYAN);
367                         scr_printf("%-50s ", buf);
368                         extract_token(buf, recs[i], 1, '|', sizeof buf);
369                         color(BRIGHT_MAGENTA);
370                         scr_printf("%-20s\n", buf);
371                         color(DIM_WHITE);
372                 }
373
374                 ch = keymenu("", "<A>dd|<D>elete|<S>ave|<Q>uit");
375                 switch (ch) {
376                 case 'a':
377                         newprompt("Enter host name: ", buf, 50);
378                         string_trim(buf);
379                         if (!IsEmptyStr(buf)) {
380                                 ++num_recs;
381                                 if (num_recs == 1) {
382                                         recs = malloc(sizeof(char *));
383                                 }
384                                 else {
385                                         recs = realloc(recs, (sizeof(char *)) * num_recs);
386                                 }
387                                 strcat(buf, "|");
388                                 get_inet_rec_type(ipc, &buf[strlen(buf)]);
389                                 recs[num_recs - 1] = strdup(buf);
390                         }
391                         modified = 1;
392                         break;
393                 case 'd':
394                         i = intprompt("Delete which one", 1, 1, num_recs) - 1;
395                         free(recs[i]);
396                         --num_recs;
397                         for (j = i; j < num_recs; ++j) {
398                                 recs[j] = recs[j + 1];
399                         }
400                         modified = 1;
401                         break;
402                 case 's':
403                         r = 1;
404                         for (i = 0; i < num_recs; i++) {
405                                 r += 1 + strlen(recs[i]);
406                         }
407                         resp = (char *) calloc(1, r);
408                         if (!resp) {
409                                 scr_printf("Can't save config - out of memory!\n");
410                                 logoff(ipc, 1);
411                         }
412                         if (num_recs)
413                                 for (i = 0; i < num_recs; i++) {
414                                         strcat(resp, recs[i]);
415                                         strcat(resp, "\n");
416                                 }
417                         r = CtdlIPCSetSystemConfigByType(ipc, INTERNETCFG, resp, buf);
418                         if (r / 100 != 4) {
419                                 scr_printf("%s\n", buf);
420                         }
421                         else {
422                                 scr_printf("Wrote %d records.\n", num_recs);
423                                 modified = 0;
424                         }
425                         free(resp);
426                         break;
427                 case 'q':
428                         quitting = !modified || boolprompt("Quit without saving", 0);
429                         break;
430                 default:
431                         break;
432                 }
433         } while (!quitting);
434
435         if (recs != NULL) {
436                 for (i = 0; i < num_recs; ++i)
437                         free(recs[i]);
438                 free(recs);
439         }
440 }
441
442
443 // Edit network configuration for room sharing, mailing lists, etc.
444 void network_config_management(CtdlIPC * ipc, char *entrytype, char *comment) {
445         char filename[PATH_MAX];
446         char changefile[PATH_MAX];
447         int e_ex_code;
448         pid_t editor_pid;
449         int cksum;
450         int b, i, tokens;
451         char buf[1024];
452         char instr[1024];
453         char addr[1024];
454         FILE *tempfp;
455         FILE *changefp;
456         char *listing = NULL;
457         int r;
458
459         if (IsEmptyStr(editor_path)) {
460                 scr_printf("You must have an external editor configured in order to use this function.\n");
461                 return;
462         }
463
464         CtdlMakeTempFileName(filename, sizeof filename);
465         CtdlMakeTempFileName(changefile, sizeof changefile);
466
467         tempfp = fopen(filename, "w");
468         if (tempfp == NULL) {
469                 scr_printf("Cannot open %s: %s\n", filename, strerror(errno));
470                 return;
471         }
472
473         fprintf(tempfp, "# Configuration for room: %s\n", room_name);
474         fprintf(tempfp, "# %s\n", comment);
475         fprintf(tempfp, "# Specify one per line.\n" "\n\n");
476
477         r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
478         if (r / 100 == 1) {
479                 while (listing && !IsEmptyStr(listing)) {
480                         extract_token(buf, listing, 0, '\n', sizeof buf);
481                         remove_token(listing, 0, '\n');
482                         extract_token(instr, buf, 0, '|', sizeof instr);
483                         if (!strcasecmp(instr, entrytype)) {
484                                 tokens = num_tokens(buf, '|');
485                                 for (i = 1; i < tokens; ++i) {
486                                         extract_token(addr, buf, i, '|', sizeof addr);
487                                         fprintf(tempfp, "%s", addr);
488                                         if (i < (tokens - 1)) {
489                                                 fprintf(tempfp, "|");
490                                         }
491                                 }
492                                 fprintf(tempfp, "\n");
493                         }
494                 }
495         }
496         if (listing) {
497                 free(listing);
498                 listing = NULL;
499         }
500         fclose(tempfp);
501
502         e_ex_code = 1;          // start with a failed exit code
503         stty_ctdl(SB_RESTORE);
504         editor_pid = fork();
505         cksum = file_checksum(filename);
506         if (editor_pid == 0) {
507                 chmod(filename, 0600);
508                 putenv("WINDOW_TITLE=Network configuration");
509                 execlp(editor_path, editor_path, filename, NULL);
510                 exit(1);
511         }
512         if (editor_pid > 0) {
513                 do {
514                         e_ex_code = 0;
515                         b = ka_wait(&e_ex_code);
516                 } while ((b != editor_pid) && (b >= 0));
517                 editor_pid = (-1);
518                 stty_ctdl(0);
519         }
520
521         if (file_checksum(filename) == cksum) {
522                 scr_printf("*** No changes to save.\n");
523                 e_ex_code = 1;
524         }
525
526         if (e_ex_code == 0) {   // Save changes
527                 changefp = fopen(changefile, "w");
528
529                 // Load all netconfig entries that are *not* of the type we are editing
530                 r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
531                 if (r / 100 == 1) {
532                         while (listing && !IsEmptyStr(listing)) {
533                                 extract_token(buf, listing, 0, '\n', sizeof buf);
534                                 remove_token(listing, 0, '\n');
535                                 extract_token(instr, buf, 0, '|', sizeof instr);
536                                 if (strcasecmp(instr, entrytype)) {
537                                         fprintf(changefp, "%s\n", buf);
538                                 }
539                         }
540                 }
541                 if (listing) {
542                         free(listing);
543                         listing = NULL;
544                 }
545
546                 // ...and merge that with the data we just edited
547                 tempfp = fopen(filename, "r");
548                 while (fgets(buf, sizeof buf, tempfp) != NULL) {
549                         for (i = 0; i < strlen(buf); ++i) {
550                                 if (buf[i] == '#')
551                                         buf[i] = 0;
552                         }
553                         string_trim(buf);
554                         if (!IsEmptyStr(buf)) {
555                                 fprintf(changefp, "%s|%s\n", entrytype, buf);
556                         }
557                 }
558                 fclose(tempfp);
559                 fclose(changefp);
560
561                 // now write it to the server...
562                 changefp = fopen(changefile, "r");
563                 if (changefp != NULL) {
564                         listing = load_message_from_file(changefp);
565                         if (listing) {
566                                 r = CtdlIPCSetRoomNetworkConfig(ipc, listing, buf);
567                                 free(listing);
568                                 listing = NULL;
569                         }
570                         fclose(changefp);
571                 }
572         }
573
574         unlink(filename);       // Delete the temporary files
575         unlink(changefile);
576 }
577
578
579 // POP3 aggregation client configuration
580 void do_pop3client_configuration(CtdlIPC * ipc) {
581         char buf[SIZ];
582         int num_recs = 0;
583         char **recs = NULL;
584         char ch;
585         int i, j;
586         int quitting = 0;
587         int modified = 0;
588         char *listing = NULL;
589         char *other_listing = NULL;
590         int r;
591         char instr[SIZ];
592
593         r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
594         if (r / 100 == 1) {
595                 while (listing && !IsEmptyStr(listing)) {
596                         extract_token(buf, listing, 0, '\n', sizeof buf);
597                         remove_token(listing, 0, '\n');
598                         extract_token(instr, buf, 0, '|', sizeof instr);
599                         if (!strcasecmp(instr, "pop3client")) {
600
601                                 ++num_recs;
602                                 if (num_recs == 1)
603                                         recs = malloc(sizeof(char *));
604                                 else
605                                         recs = realloc(recs, (sizeof(char *)) * num_recs);
606                                 recs[num_recs - 1] = malloc(SIZ);
607                                 strcpy(recs[num_recs - 1], buf);
608
609                         }
610                 }
611         }
612         if (listing) {
613                 free(listing);
614                 listing = NULL;
615         }
616
617         do {
618                 scr_printf("\n");
619                 color(BRIGHT_WHITE);
620                 scr_printf("### " "      Remote POP3 host       " "         User name           " "Keep on server? " "\n");
621                 color(DIM_WHITE);
622                 scr_printf("--- " "---------------------------- " "---------------------------- " "--------------- " "\n");
623                 for (i = 0; i < num_recs; ++i) {
624                         color(DIM_WHITE);
625                         scr_printf("%3d ", i + 1);
626
627                         extract_token(buf, recs[i], 1, '|', sizeof buf);
628                         color(BRIGHT_CYAN);
629                         scr_printf("%-28s ", buf);
630
631                         extract_token(buf, recs[i], 2, '|', sizeof buf);
632                         color(BRIGHT_MAGENTA);
633                         scr_printf("%-28s ", buf);
634
635                         color(BRIGHT_CYAN);
636                         scr_printf("%-15s\n", (extract_int(recs[i], 4) ? "Yes" : "No"));
637                         color(DIM_WHITE);
638                 }
639
640                 ch = keymenu("", "<A>dd|<D>elete|<S>ave|<Q>uit");
641                 switch (ch) {
642                 case 'a':
643                         ++num_recs;
644                         if (num_recs == 1) {
645                                 recs = malloc(sizeof(char *));
646                         }
647                         else {
648                                 recs = realloc(recs, (sizeof(char *)) * num_recs);
649                         }
650                         strcpy(buf, "pop3client|");
651                         newprompt("Enter host name: ", &buf[strlen(buf)], 28);
652                         strcat(buf, "|");
653                         newprompt("Enter user name: ", &buf[strlen(buf)], 28);
654                         strcat(buf, "|");
655                         newprompt("Enter password : ", &buf[strlen(buf)], 16);
656                         strcat(buf, "|");
657                         scr_printf("Keep messages on server instead of deleting them? ");
658                         sprintf(&buf[strlen(buf)], "%d", yesno());
659                         strcat(buf, "|");
660                         recs[num_recs - 1] = strdup(buf);
661                         modified = 1;
662                         break;
663                 case 'd':
664                         i = intprompt("Delete which one", 1, 1, num_recs) - 1;
665                         free(recs[i]);
666                         --num_recs;
667                         for (j = i; j < num_recs; ++j)
668                                 recs[j] = recs[j + 1];
669                         modified = 1;
670                         break;
671                 case 's':
672                         r = 1;
673                         for (i = 0; i < num_recs; ++i) {
674                                 r += 1 + strlen(recs[i]);
675                         }
676                         listing = (char *) calloc(1, r);
677                         if (!listing) {
678                                 scr_printf("Can't save config - out of memory!\n");
679                                 logoff(ipc, 1);
680                         }
681                         if (num_recs)
682                                 for (i = 0; i < num_recs; ++i) {
683                                         strcat(listing, recs[i]);
684                                         strcat(listing, "\n");
685                                 }
686
687                         // Retrieve all the *other* records for merging
688                         r = CtdlIPCGetRoomNetworkConfig(ipc, &other_listing, buf);
689                         if (r / 100 == 1) {
690                                 for (i = 0; i < num_tokens(other_listing, '\n'); ++i) {
691                                         extract_token(buf, other_listing, i, '\n', sizeof buf);
692                                         if (strncasecmp(buf, "pop3client|", 11)) {
693                                                 listing = realloc(listing, strlen(listing) + strlen(buf) + 10);
694                                                 strcat(listing, buf);
695                                                 strcat(listing, "\n");
696                                         }
697                                 }
698                         }
699                         free(other_listing);
700                         r = CtdlIPCSetRoomNetworkConfig(ipc, listing, buf);
701                         free(listing);
702                         listing = NULL;
703
704                         if (r / 100 != 4) {
705                                 scr_printf("%s\n", buf);
706                         }
707                         else {
708                                 scr_printf("Wrote %d records.\n", num_recs);
709                                 modified = 0;
710                         }
711                         quitting = 1;
712                         break;
713                 case 'q':
714                         quitting = !modified || boolprompt("Quit without saving", 0);
715                         break;
716                 default:
717                         break;
718                 }
719         } while (!quitting);
720
721         if (recs != NULL) {
722                 for (i = 0; i < num_recs; ++i) {
723                         free(recs[i]);
724                 }
725                 free(recs);
726         }
727 }
728
729
730 // RSS feed retrieval client configuration
731 void do_rssclient_configuration(CtdlIPC * ipc) {
732         char buf[SIZ];
733         int num_recs = 0;
734         char **recs = NULL;
735         char ch;
736         int i, j;
737         int quitting = 0;
738         int modified = 0;
739         char *listing = NULL;
740         char *other_listing = NULL;
741         int r;
742         char instr[SIZ];
743
744         r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
745         if (r / 100 == 1) {
746                 while (listing && !IsEmptyStr(listing)) {
747                         extract_token(buf, listing, 0, '\n', sizeof buf);
748                         remove_token(listing, 0, '\n');
749                         extract_token(instr, buf, 0, '|', sizeof instr);
750                         if (!strcasecmp(instr, "rssclient")) {
751
752                                 ++num_recs;
753                                 if (num_recs == 1)
754                                         recs = malloc(sizeof(char *));
755                                 else
756                                         recs = realloc(recs, (sizeof(char *)) * num_recs);
757                                 recs[num_recs - 1] = malloc(SIZ);
758                                 strcpy(recs[num_recs - 1], buf);
759
760                         }
761                 }
762         }
763         if (listing) {
764                 free(listing);
765                 listing = NULL;
766         }
767
768         do {
769                 scr_printf("\n");
770                 color(BRIGHT_WHITE);
771                 scr_printf("### Feed URL\n");
772                 color(DIM_WHITE);
773                 scr_printf("--- " "---------------------------------------------------------------------------" "\n");
774
775                 for (i = 0; i < num_recs; ++i) {
776                         color(DIM_WHITE);
777                         scr_printf("%3d ", i + 1);
778
779                         extract_token(buf, recs[i], 1, '|', sizeof buf);
780                         color(BRIGHT_CYAN);
781                         scr_printf("%-75s\n", buf);
782
783                         color(DIM_WHITE);
784                 }
785
786                 ch = keymenu("", "<A>dd|<D>elete|<S>ave|<Q>uit");
787                 switch (ch) {
788                 case 'a':
789                         ++num_recs;
790                         if (num_recs == 1) {
791                                 recs = malloc(sizeof(char *));
792                         }
793                         else {
794                                 recs = realloc(recs, (sizeof(char *)) * num_recs);
795                         }
796                         strcpy(buf, "rssclient|");
797                         newprompt("Enter feed URL: ", &buf[strlen(buf)], 75);
798                         strcat(buf, "|");
799                         recs[num_recs - 1] = strdup(buf);
800                         modified = 1;
801                         break;
802                 case 'd':
803                         i = intprompt("Delete which one", 1, 1, num_recs) - 1;
804                         free(recs[i]);
805                         --num_recs;
806                         for (j = i; j < num_recs; ++j)
807                                 recs[j] = recs[j + 1];
808                         modified = 1;
809                         break;
810                 case 's':
811                         r = 1;
812                         for (i = 0; i < num_recs; ++i) {
813                                 r += 1 + strlen(recs[i]);
814                         }
815                         listing = (char *) calloc(1, r);
816                         if (!listing) {
817                                 scr_printf("Can't save config - out of memory!\n");
818                                 logoff(ipc, 1);
819                         }
820                         if (num_recs)
821                                 for (i = 0; i < num_recs; ++i) {
822                                         strcat(listing, recs[i]);
823                                         strcat(listing, "\n");
824                                 }
825
826                         // Retrieve all the *other* records for merging
827                         r = CtdlIPCGetRoomNetworkConfig(ipc, &other_listing, buf);
828                         if (r / 100 == 1) {
829                                 for (i = 0; i < num_tokens(other_listing, '\n'); ++i) {
830                                         extract_token(buf, other_listing, i, '\n', sizeof buf);
831                                         if (strncasecmp(buf, "rssclient|", 10)) {
832                                                 listing = realloc(listing, strlen(listing) + strlen(buf) + 10);
833                                                 strcat(listing, buf);
834                                                 strcat(listing, "\n");
835                                         }
836                                 }
837                         }
838                         free(other_listing);
839                         r = CtdlIPCSetRoomNetworkConfig(ipc, listing, buf);
840                         free(listing);
841                         listing = NULL;
842
843                         if (r / 100 != 4) {
844                                 scr_printf("%s\n", buf);
845                         }
846                         else {
847                                 scr_printf("Wrote %d records.\n", num_recs);
848                                 modified = 0;
849                         }
850                         quitting = 1;
851                         break;
852                 case 'q':
853                         quitting = !modified || boolprompt("Quit without saving", 0);
854                         break;
855                 default:
856                         break;
857                 }
858         } while (!quitting);
859
860         if (recs != NULL) {
861                 for (i = 0; i < num_recs; ++i)
862                         free(recs[i]);
863                 free(recs);
864         }
865 }