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