2 * Configuration screens that are part of the text mode client.
4 * Copyright (c) 1987-2018 by the citadel.org team
6 * This program is open source software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
15 #include "textclient.h"
18 extern char tempdir[];
19 extern char *axdefs[8];
20 extern long highest_msg_read;
21 extern long maxmsgnum;
22 extern unsigned room_flags;
23 extern int screenwidth;
24 char editor_path[PATH_MAX];
28 * General system configuration command
30 void do_system_configuration(CtdlIPC *ipc)
33 /* NUM_CONFIGS is now defined in citadel.h */
36 char sc[NUM_CONFIGS][256];
38 struct ExpirePolicy *site_expirepolicy = NULL;
39 struct ExpirePolicy *mbx_expirepolicy = NULL;
42 int r; /* IPC response code */
43 int server_configs = 0;
45 /* Clear out the config buffers */
46 memset(&sc[0][0], 0, sizeof(sc));
48 /* Fetch the current config */
49 r = CtdlIPCGetSystemConfig(ipc, &resp, buf);
51 server_configs = num_tokens(resp, '\n');
52 for (a=0; a<server_configs; ++a) {
53 if (a < NUM_CONFIGS) {
54 extract_token(&sc[a][0], resp, a, '\n', sizeof sc[a]);
60 /* Fetch the expire policy (this will silently fail on old servers,
61 * resulting in "default" policy)
63 r = CtdlIPCGetMessageExpirationPolicy(ipc, 2, &site_expirepolicy, buf);
64 r = CtdlIPCGetMessageExpirationPolicy(ipc, 3, &mbx_expirepolicy, buf);
66 /* Identification parameters */
68 strprompt("Node name", &sc[0][0], 15);
69 strprompt("Fully qualified domain name", &sc[1][0], 63);
70 strprompt("Human readable node name", &sc[2][0], 20);
71 strprompt("Telephone number", &sc[3][0], 15);
72 strprompt("Geographic location of this system", &sc[12][0], 31);
73 strprompt("Name of system administrator", &sc[13][0], 25);
74 strprompt("Paginator prompt", &sc[10][0], 79);
76 /* Security parameters */
78 snprintf(sc[7], sizeof sc[7], "%d", (boolprompt("Require registration for new users", atoi(&sc[7][0]))));
79 snprintf(sc[29], sizeof sc[29], "%d", (boolprompt("Disable self-service user account creation", atoi(&sc[29][0]))));
80 strprompt("Initial access level for new users", &sc[6][0], 1);
81 strprompt("Access level required to create rooms", &sc[19][0], 1);
82 snprintf(sc[67], sizeof sc[67], "%d", (boolprompt("Allow anonymous guest logins", atoi(&sc[67][0]))));
83 snprintf(sc[4], sizeof sc[4], "%d", (boolprompt(
84 "Automatically give room admin privs to a user who creates a private room",
87 snprintf(sc[8], sizeof sc[8], "%d", (boolprompt(
88 "Automatically move problem user messages to twit room",
91 strprompt("Name of twit room", &sc[9][0], ROOMNAMELEN);
92 snprintf(sc[11], sizeof sc[11], "%d", (boolprompt(
93 "Restrict Internet mail to only those with that privilege",
95 snprintf(sc[26], sizeof sc[26], "%d", (boolprompt(
96 "Allow admins to Zap (forget) rooms",
99 if (!IsEmptyStr(&sc[18][0])) {
105 logpages = boolprompt("Log all instant messages", logpages);
107 strprompt("Name of logging room", &sc[18][0], ROOMNAMELEN);
113 /* Commented out because this setting isn't really appropriate to
114 * change while the server is running.
116 * snprintf(sc[52], sizeof sc[52], "%d", (boolprompt(
117 * "Use system authentication",
118 * atoi(&sc[52][0]))));
123 strprompt("Server connection idle timeout (in seconds)", &sc[5][0], 4);
124 strprompt("Maximum concurrent sessions", &sc[14][0], 4);
125 strprompt("Maximum message length", &sc[20][0], 20);
126 strprompt("Minimum number of worker threads", &sc[21][0], 3);
127 strprompt("Maximum number of worker threads", &sc[22][0], 3);
128 snprintf(sc[43], sizeof sc[43], "%d", (boolprompt(
129 "Automatically delete committed database logs",
132 strprompt("Server IP address (* for 'any')", &sc[37][0], 15);
133 strprompt("POP3 server port (-1 to disable)", &sc[23][0], 5);
134 strprompt("POP3S server port (-1 to disable)", &sc[40][0], 5);
135 strprompt("IMAP server port (-1 to disable)", &sc[27][0], 5);
136 strprompt("IMAPS server port (-1 to disable)", &sc[39][0], 5);
137 strprompt("SMTP MTA server port (-1 to disable)", &sc[24][0], 5);
138 strprompt("SMTP MSA server port (-1 to disable)", &sc[38][0], 5);
139 strprompt("SMTPS server port (-1 to disable)", &sc[41][0], 5);
140 strprompt("NNTP server port (-1 to disable)", &sc[70][0], 5);
141 strprompt("NNTPS server port (-1 to disable)", &sc[71][0], 5);
142 strprompt("Postfix TCP Dictionary Port server port (-1 to disable)", &sc[50][0], 5);
143 strprompt("ManageSieve server port (-1 to disable)", &sc[51][0], 5);
145 strprompt("XMPP (Jabber) client to server port (-1 to disable)", &sc[62][0], 5);
146 /* No prompt because we don't implement this service yet, it's just a placeholder */
147 /* strprompt("XMPP (Jabber) server to server port (-1 to disable)", &sc[63][0], 5); */
149 /* This logic flips the question around, because it's one of those
150 * situations where 0=yes and 1=no
154 a = boolprompt("Correct forged From: lines during authenticated SMTP", a);
156 snprintf(sc[25], sizeof sc[25], "%d", a);
158 snprintf(sc[66], sizeof sc[66], "%d", (boolprompt(
159 "Flag messages as spam instead of rejecting",
162 /* This logic flips the question around, because it's one of those
163 * situations where 0=yes and 1=no
167 a = boolprompt("Force IMAP posts in public rooms to be from the user who submitted them", a);
169 snprintf(sc[61], sizeof sc[61], "%d", a);
171 snprintf(sc[45], sizeof sc[45], "%d", (boolprompt(
172 "Allow unauthenticated SMTP clients to spoof my domains",
174 snprintf(sc[57], sizeof sc[57], "%d", (boolprompt(
175 "Perform RBL checks at greeting instead of after RCPT",
179 if (ipc->ServInfo.supports_ldap) {
180 a = strlen(&sc[32][0]);
181 a = (a ? 1 : 0); /* Set only to 1 or 0 */
182 a = boolprompt("Do you want to configure LDAP authentication?", a);
184 strprompt("Host name of LDAP server", &sc[32][0], 127);
185 strprompt("Port number of LDAP service", &sc[33][0], 5);
186 strprompt("Base DN", &sc[34][0], 255);
187 strprompt("Bind DN (or blank for anonymous bind)", &sc[35][0], 255);
188 strprompt("Password for bind DN (or blank for anonymous bind)", &sc[36][0], 255);
191 strcpy(&sc[32][0], "");
195 /* Expiry settings */
196 strprompt("Default user purge time (days)", &sc[16][0], 5);
197 strprompt("Default room purge time (days)", &sc[17][0], 5);
199 /* Angels and demons dancing in my head... */
201 snprintf(buf, sizeof buf, "%d", site_expirepolicy->expire_mode);
202 strprompt("System default message expire policy (? for list)",
206 "1. Never automatically expire messages\n"
207 "2. Expire by message count\n"
208 "3. Expire by message age\n");
210 } while ((buf[0] < '1') || (buf[0] > '3'));
211 site_expirepolicy->expire_mode = buf[0] - '0';
213 /* ...lunatics and monsters underneath my bed */
214 if (site_expirepolicy->expire_mode == 2) {
215 snprintf(buf, sizeof buf, "%d", site_expirepolicy->expire_value);
216 strprompt("Keep how many messages online?", buf, 10);
217 site_expirepolicy->expire_value = atol(buf);
219 if (site_expirepolicy->expire_mode == 3) {
220 snprintf(buf, sizeof buf, "%d", site_expirepolicy->expire_value);
221 strprompt("Keep messages for how many days?", buf, 10);
222 site_expirepolicy->expire_value = atol(buf);
225 /* Media messiahs preying on my fears... */
227 snprintf(buf, sizeof buf, "%d", mbx_expirepolicy->expire_mode);
228 strprompt("Mailbox default message expire policy (? for list)",
232 "0. Go with the system default\n"
233 "1. Never automatically expire messages\n"
234 "2. Expire by message count\n"
235 "3. Expire by message age\n");
237 } while ((buf[0] < '0') || (buf[0] > '3'));
238 mbx_expirepolicy->expire_mode = buf[0] - '0';
240 /* ...Pop culture prophets playing in my ears */
241 if (mbx_expirepolicy->expire_mode == 2) {
242 snprintf(buf, sizeof buf, "%d", mbx_expirepolicy->expire_value);
243 strprompt("Keep how many messages online?", buf, 10);
244 mbx_expirepolicy->expire_value = atol(buf);
246 if (mbx_expirepolicy->expire_mode == 3) {
247 snprintf(buf, sizeof buf, "%d", mbx_expirepolicy->expire_value);
248 strprompt("Keep messages for how many days?", buf, 10);
249 mbx_expirepolicy->expire_value = atol(buf);
252 strprompt("How often to run network jobs (in seconds)", &sc[28][0], 5);
253 strprompt("Default frequency to run POP3 collection (in seconds)", &sc[64][0], 5);
254 strprompt("Fastest frequency to run POP3 collection (in seconds)", &sc[65][0], 5);
255 strprompt("Hour to run purges (0-23)", &sc[31][0], 2);
256 snprintf(sc[42], sizeof sc[42], "%d", (boolprompt(
257 "Enable full text search index (warning: resource intensive)",
260 snprintf(sc[46], sizeof sc[46], "%d", (boolprompt(
261 "Perform journaling of email messages",
263 snprintf(sc[47], sizeof sc[47], "%d", (boolprompt(
264 "Perform journaling of non-email messages",
266 if ( (atoi(&sc[46][0])) || (atoi(&sc[47][0])) ) {
267 strprompt("Email destination of journalized messages",
271 /* No more Funambol */
277 /* External pager stuff */
279 if (strlen(sc[60]) > 0) yes_pager = 1;
280 yes_pager = boolprompt("Configure an external pager tool", yes_pager);
282 strprompt("External pager tool", &sc[60][0], 255);
288 /* Master user account */
290 if (strlen(sc[58]) > 0) yes_muacct = 1;
291 yes_muacct = boolprompt("Enable a 'master user' account", yes_muacct);
293 strprompt("Master user name", &sc[58][0], 31);
294 strprompt("Master user password", &sc[59][0], -31);
297 strcpy(&sc[58][0], "");
298 strcpy(&sc[59][0], "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
302 scr_printf("Save this configuration? ");
305 for (a = 0; a < NUM_CONFIGS; a++) {
306 r += 1 + strlen(sc[a]);
308 resp = (char *)calloc(1, r);
310 scr_printf("Can't save config - out of memory!\n");
313 for (a = 0; a < NUM_CONFIGS; a++) {
317 r = CtdlIPCSetSystemConfig(ipc, resp, buf);
319 scr_printf("%s\n", buf);
323 r = CtdlIPCSetMessageExpirationPolicy(ipc, 2, site_expirepolicy, buf);
325 scr_printf("%s\n", buf);
328 r = CtdlIPCSetMessageExpirationPolicy(ipc, 3, mbx_expirepolicy, buf);
330 scr_printf("%s\n", buf);
334 if (site_expirepolicy) free(site_expirepolicy);
335 if (mbx_expirepolicy) free(mbx_expirepolicy);
340 * support function for do_internet_configuration()
342 void get_inet_rec_type(CtdlIPC *ipc, char *buf) {
345 keyopt(" <1> localhost (Alias for this computer)\n");
346 keyopt(" <2> smart host (Forward all outbound mail to this host)\n");
347 keyopt(" <3> fallback host (Send mail to this host only if direct delivery fails)\n");
348 keyopt(" <4> SpamAssassin (Address of SpamAssassin server)\n");
349 keyopt(" <5> RBL (domain suffix of spam hunting RBL)\n");
350 keyopt(" <6> masq domains (Domains as which users are allowed to masquerade)\n");
351 keyopt(" <7> ClamAV (Address of ClamAV clamd server)\n");
352 sel = intprompt("Which one", 1, 1, 8);
354 case 1: strcpy(buf, "localhost");
356 case 2: strcpy(buf, "smarthost");
358 case 3: strcpy(buf, "fallbackhost");
360 case 4: strcpy(buf, "spamassassin");
362 case 5: strcpy(buf, "rbl");
364 case 6: strcpy(buf, "masqdomain");
366 case 7: strcpy(buf, "clamav");
373 * Internet mail configuration
375 void do_internet_configuration(CtdlIPC *ipc)
387 r = CtdlIPCGetSystemConfigByType(ipc, INTERNETCFG, &resp, buf);
389 while (!IsEmptyStr(resp)) {
390 extract_token(buf, resp, 0, '\n', sizeof buf);
391 remove_token(resp, 0, '\n');
393 // VILE SLEAZY HACK: replace obsolete "directory" domains with "localhost"
394 char *d = strstr(buf, "|directory");
396 strcpy(d, "|localhost");
400 if (num_recs == 1) recs = malloc(sizeof(char *));
401 else recs = realloc(recs, (sizeof(char *)) * num_recs);
402 recs[num_recs-1] = malloc(strlen(buf) + 1);
403 strcpy(recs[num_recs-1], buf);
406 if (resp) free(resp);
411 scr_printf("### Host or domain Record type \n");
413 scr_printf("--- -------------------------------------------------- --------------------\n");
414 for (i=0; i<num_recs; ++i) {
416 scr_printf("%3d ", i+1);
417 extract_token(buf, recs[i], 0, '|', sizeof buf);
419 scr_printf("%-50s ", buf);
420 extract_token(buf, recs[i], 1, '|', sizeof buf);
421 color(BRIGHT_MAGENTA);
422 scr_printf("%-20s\n", buf);
426 ch = keymenu("", "<A>dd|<D>elete|<S>ave|<Q>uit");
429 newprompt("Enter host name: ", buf, 50);
431 if (!IsEmptyStr(buf)) {
434 recs = malloc(sizeof(char *));
437 recs = realloc(recs, (sizeof(char *)) * num_recs);
440 get_inet_rec_type(ipc, &buf[strlen(buf)]);
441 recs[num_recs-1] = strdup(buf);
446 i = intprompt("Delete which one", 1, 1, num_recs) - 1;
449 for (j=i; j<num_recs; ++j) {
456 for (i = 0; i < num_recs; i++)
457 r += 1 + strlen(recs[i]);
458 resp = (char *)calloc(1, r);
460 scr_printf("Can't save config - out of memory!\n");
463 if (num_recs) for (i = 0; i < num_recs; i++) {
464 strcat(resp, recs[i]);
467 r = CtdlIPCSetSystemConfigByType(ipc, INTERNETCFG, resp, buf);
469 scr_printf("%s\n", buf);
471 scr_printf("Wrote %d records.\n", num_recs);
477 quitting = !modified || boolprompt(
478 "Quit without saving", 0);
486 for (i=0; i<num_recs; ++i) free(recs[i]);
494 * Edit network configuration for room sharing, mailing lists, etc.
496 void network_config_management(CtdlIPC *ipc, char *entrytype, char *comment)
498 char filename[PATH_MAX];
499 char changefile[PATH_MAX];
509 char *listing = NULL;
512 if (IsEmptyStr(editor_path)) {
513 scr_printf("You must have an external editor configured in order to use this function.\n");
517 CtdlMakeTempFileName(filename, sizeof filename);
518 CtdlMakeTempFileName(changefile, sizeof changefile);
520 tempfp = fopen(filename, "w");
521 if (tempfp == NULL) {
522 scr_printf("Cannot open %s: %s\n", filename, strerror(errno));
526 fprintf(tempfp, "# Configuration for room: %s\n", room_name);
527 fprintf(tempfp, "# %s\n", comment);
528 fprintf(tempfp, "# Specify one per line.\n"
531 r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
533 while(listing && !IsEmptyStr(listing)) {
534 extract_token(buf, listing, 0, '\n', sizeof buf);
535 remove_token(listing, 0, '\n');
536 extract_token(instr, buf, 0, '|', sizeof instr);
537 if (!strcasecmp(instr, entrytype)) {
538 tokens = num_tokens(buf, '|');
539 for (i=1; i<tokens; ++i) {
540 extract_token(addr, buf, i, '|', sizeof addr);
541 fprintf(tempfp, "%s", addr);
542 if (i < (tokens-1)) {
543 fprintf(tempfp, "|");
546 fprintf(tempfp, "\n");
556 e_ex_code = 1; /* start with a failed exit code */
557 stty_ctdl(SB_RESTORE);
559 cksum = file_checksum(filename);
560 if (editor_pid == 0) {
561 chmod(filename, 0600);
562 putenv("WINDOW_TITLE=Network configuration");
563 execlp(editor_path, editor_path, filename, NULL);
566 if (editor_pid > 0) {
569 b = ka_wait(&e_ex_code);
570 } while ((b != editor_pid) && (b >= 0));
575 if (file_checksum(filename) == cksum) {
576 scr_printf("*** No changes to save.\n");
580 if (e_ex_code == 0) { /* Save changes */
581 changefp = fopen(changefile, "w");
583 /* Load all netconfig entries that are *not* of the type we are editing */
584 r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
586 while(listing && !IsEmptyStr(listing)) {
587 extract_token(buf, listing, 0, '\n', sizeof buf);
588 remove_token(listing, 0, '\n');
589 extract_token(instr, buf, 0, '|', sizeof instr);
590 if (strcasecmp(instr, entrytype)) {
591 fprintf(changefp, "%s\n", buf);
600 /* ...and merge that with the data we just edited */
601 tempfp = fopen(filename, "r");
602 while (fgets(buf, sizeof buf, tempfp) != NULL) {
603 for (i=0; i<strlen(buf); ++i) {
604 if (buf[i] == '#') buf[i] = 0;
607 if (!IsEmptyStr(buf)) {
608 fprintf(changefp, "%s|%s\n", entrytype, buf);
614 /* now write it to the server... */
615 changefp = fopen(changefile, "r");
616 if (changefp != NULL) {
617 listing = load_message_from_file(changefp);
619 r = CtdlIPCSetRoomNetworkConfig(ipc, listing, buf);
627 unlink(filename); /* Delete the temporary files */
633 * POP3 aggregation client configuration
635 void do_pop3client_configuration(CtdlIPC *ipc)
644 char *listing = NULL;
645 char *other_listing = NULL;
649 r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
651 while(listing && !IsEmptyStr(listing)) {
652 extract_token(buf, listing, 0, '\n', sizeof buf);
653 remove_token(listing, 0, '\n');
654 extract_token(instr, buf, 0, '|', sizeof instr);
655 if (!strcasecmp(instr, "pop3client")) {
658 if (num_recs == 1) recs = malloc(sizeof(char *));
659 else recs = realloc(recs, (sizeof(char *)) * num_recs);
660 recs[num_recs-1] = malloc(SIZ);
661 strcpy(recs[num_recs-1], buf);
681 "---------------------------- "
682 "---------------------------- "
685 for (i=0; i<num_recs; ++i) {
687 scr_printf("%3d ", i+1);
689 extract_token(buf, recs[i], 1, '|', sizeof buf);
691 scr_printf("%-28s ", buf);
693 extract_token(buf, recs[i], 2, '|', sizeof buf);
694 color(BRIGHT_MAGENTA);
695 scr_printf("%-28s ", buf);
698 scr_printf("%-15s\n", (extract_int(recs[i], 4) ? "Yes" : "No") );
702 ch = keymenu("", "<A>dd|<D>elete|<S>ave|<Q>uit");
707 recs = malloc(sizeof(char *));
710 recs = realloc(recs, (sizeof(char *)) * num_recs);
712 strcpy(buf, "pop3client|");
713 newprompt("Enter host name: ", &buf[strlen(buf)], 28);
715 newprompt("Enter user name: ", &buf[strlen(buf)], 28);
717 newprompt("Enter password : ", &buf[strlen(buf)], 16);
719 scr_printf("Keep messages on server instead of deleting them? ");
720 sprintf(&buf[strlen(buf)], "%d", yesno());
722 recs[num_recs-1] = strdup(buf);
726 i = intprompt("Delete which one",
730 for (j=i; j<num_recs; ++j)
736 for (i = 0; i < num_recs; ++i) {
737 r += 1 + strlen(recs[i]);
739 listing = (char*) calloc(1, r);
741 scr_printf("Can't save config - out of memory!\n");
744 if (num_recs) for (i = 0; i < num_recs; ++i) {
745 strcat(listing, recs[i]);
746 strcat(listing, "\n");
749 /* Retrieve all the *other* records for merging */
750 r = CtdlIPCGetRoomNetworkConfig(ipc, &other_listing, buf);
752 for (i=0; i<num_tokens(other_listing, '\n'); ++i) {
753 extract_token(buf, other_listing, i, '\n', sizeof buf);
754 if (strncasecmp(buf, "pop3client|", 11)) {
755 listing = realloc(listing, strlen(listing) +
757 strcat(listing, buf);
758 strcat(listing, "\n");
763 r = CtdlIPCSetRoomNetworkConfig(ipc, listing, buf);
768 scr_printf("%s\n", buf);
770 scr_printf("Wrote %d records.\n", num_recs);
776 quitting = !modified || boolprompt(
777 "Quit without saving", 0);
785 for (i=0; i<num_recs; ++i) free(recs[i]);
796 * RSS feed retrieval client configuration
798 void do_rssclient_configuration(CtdlIPC *ipc)
807 char *listing = NULL;
808 char *other_listing = NULL;
812 r = CtdlIPCGetRoomNetworkConfig(ipc, &listing, buf);
814 while(listing && !IsEmptyStr(listing)) {
815 extract_token(buf, listing, 0, '\n', sizeof buf);
816 remove_token(listing, 0, '\n');
817 extract_token(instr, buf, 0, '|', sizeof instr);
818 if (!strcasecmp(instr, "rssclient")) {
821 if (num_recs == 1) recs = malloc(sizeof(char *));
822 else recs = realloc(recs, (sizeof(char *)) * num_recs);
823 recs[num_recs-1] = malloc(SIZ);
824 strcpy(recs[num_recs-1], buf);
837 scr_printf("### Feed URL\n");
840 "---------------------------------------------------------------------------"
843 for (i=0; i<num_recs; ++i) {
845 scr_printf("%3d ", i+1);
847 extract_token(buf, recs[i], 1, '|', sizeof buf);
849 scr_printf("%-75s\n", buf);
854 ch = keymenu("", "<A>dd|<D>elete|<S>ave|<Q>uit");
859 recs = malloc(sizeof(char *));
862 recs = realloc(recs, (sizeof(char *)) * num_recs);
864 strcpy(buf, "rssclient|");
865 newprompt("Enter feed URL: ", &buf[strlen(buf)], 75);
867 recs[num_recs-1] = strdup(buf);
871 i = intprompt("Delete which one", 1, 1, num_recs) - 1;
874 for (j=i; j<num_recs; ++j)
880 for (i = 0; i < num_recs; ++i) {
881 r += 1 + strlen(recs[i]);
883 listing = (char*) calloc(1, r);
885 scr_printf("Can't save config - out of memory!\n");
888 if (num_recs) for (i = 0; i < num_recs; ++i) {
889 strcat(listing, recs[i]);
890 strcat(listing, "\n");
893 /* Retrieve all the *other* records for merging */
894 r = CtdlIPCGetRoomNetworkConfig(ipc, &other_listing, buf);
896 for (i=0; i<num_tokens(other_listing, '\n'); ++i) {
897 extract_token(buf, other_listing, i, '\n', sizeof buf);
898 if (strncasecmp(buf, "rssclient|", 10)) {
899 listing = realloc(listing, strlen(listing) +
901 strcat(listing, buf);
902 strcat(listing, "\n");
907 r = CtdlIPCSetRoomNetworkConfig(ipc, listing, buf);
912 scr_printf("%s\n", buf);
914 scr_printf("Wrote %d records.\n", num_recs);
920 quitting = !modified || boolprompt(
921 "Quit without saving", 0);
929 for (i=0; i<num_recs; ++i) free(recs[i]);