1 // Text client functions for reading and writing of messages
3 // Beware: this is really old and crappy code, written in the
4 // late 1980s when my coding style was absolute garbage. It
5 // works, but we probably should replace most of it.
7 // Copyright (c) 1987-2024 by the citadel.org team
9 // This program is open source software. Use, duplication, or disclosure is subject to the GNU General Public License version 3.
11 #include "textclient.h"
13 #define MAXWORDBUF SIZ
14 #define NO_REPLY_TO "nobody ... xxxxxx"
17 char reply_subject[SIZ];
18 char reply_references[SIZ];
19 char reply_inreplyto[SIZ];
23 char text[MAXWORDBUF];
26 void stty_ctdl(int cmd);
27 int haschar(const char *st, int ch);
28 int file_checksum(char *filename);
29 void progress(CtdlIPC * ipc, unsigned long curr, unsigned long cmax);
31 unsigned long *msg_arr = NULL;
34 extern char room_name[];
35 extern char tempdir[];
36 extern unsigned room_flags;
37 extern unsigned room_flags2;
39 extern long highest_msg_read;
42 extern int screenwidth;
43 extern int screenheight;
44 extern long maxmsgnum;
47 extern char is_room_aide;
48 extern char fullname[];
50 extern unsigned userflags;
51 extern char sigcaught;
52 extern char printcmd[];
53 extern int rc_allow_attachments;
54 extern int rc_display_message_numbers;
55 extern int rc_force_mail_prompts;
57 extern int editor_pid;
58 extern CtdlIPC *ipc_for_signal_handlers; // KLUDGE cover your eyes
60 char urls[MAXURLS][SIZ];
62 int has_images = 0; // Current msg has images
63 struct parts *last_message_parts = NULL; // Parts from last msg
66 void ka_sigcatch(int signum) {
68 signal(SIGALRM, ka_sigcatch);
69 CtdlIPCNoop(ipc_for_signal_handlers);
73 // server keep-alive version of wait() (needed for external editor)
74 pid_t ka_wait(int *kstatus) {
78 signal(SIGALRM, ka_sigcatch);
82 } while (errno == EINTR);
83 signal(SIGALRM, SIG_IGN);
89 // version of system() that uses ka_wait()
90 int ka_system(char *shc) {
98 perror("Cannot fork");
100 return ((pid_t) childpid);
104 execlp("/bin/sh", "sh", "-c", shc, NULL);
110 waitpid = ka_wait(&retcode);
111 } while (waitpid != childpid);
119 // add a newline to the buffer...
120 void add_newline(struct cittext *textlist) {
124 while (ptr->next != NULL)
127 while (ptr->text[strlen(ptr->text) - 1] == 32)
128 ptr->text[strlen(ptr->text) - 1] = 0;
130 ptr->next = (struct cittext *)
131 malloc(sizeof(struct cittext));
134 strcpy(ptr->text, "");
138 // add a word to the buffer...
139 void add_word(struct cittext *textlist, char *wordbuf) {
143 while (ptr->next != NULL)
146 if (3 + strlen(ptr->text) + strlen(wordbuf) > screenwidth) {
147 ptr->next = (struct cittext *)
148 malloc(sizeof(struct cittext));
151 strcpy(ptr->text, "");
154 strcat(ptr->text, wordbuf);
155 strcat(ptr->text, " ");
159 // begin editing of an opened file pointed to by fp
160 void citedit(FILE * fp) {
161 int a, prev, finished, b, last_space;
163 struct cittext *textlist = NULL;
165 char wordbuf[MAXWORDBUF];
168 // first, load the text into the buffer
170 textlist = (struct cittext *) malloc(sizeof(struct cittext));
171 textlist->next = NULL;
172 strcpy(textlist->text, "");
176 while (a = getc(fp), a >= 0) {
178 if ((a == 32) || (a == 9) || (a == 13) || (a == 10)) {
179 add_word(textlist, wordbuf);
181 if ((prev == 13) || (prev == 10)) {
182 add_word(textlist, "\n");
183 add_newline(textlist);
184 add_word(textlist, "");
188 wordbuf[strlen(wordbuf) + 1] = 0;
189 wordbuf[strlen(wordbuf)] = a;
191 if (strlen(wordbuf) + 3 > screenwidth) {
192 add_word(textlist, wordbuf);
200 prev = (appending ? 13 : (-1));
211 if ((a != 32) && (prev == 13)) {
212 add_word(textlist, "\n");
216 if ((a == 32) && (prev == 13)) {
217 add_word(textlist, "\n");
218 add_newline(textlist);
222 if (!IsEmptyStr(wordbuf)) {
223 wordbuf[strlen(wordbuf) - 1] = 0;
231 wordbuf[strlen(wordbuf) - 1] = 0;
235 } while (!IsEmptyStr(wordbuf) && wordbuf[strlen(wordbuf) - 1] != ' ');
239 if (IsEmptyStr(wordbuf))
242 for (b = 0; b < strlen(wordbuf); ++b)
243 if (wordbuf[b] == 32) {
245 add_word(textlist, wordbuf);
246 strcpy(wordbuf, &wordbuf[b + 1]);
249 add_word(textlist, wordbuf);
255 wordbuf[strlen(wordbuf) + 1] = 0;
256 wordbuf[strlen(wordbuf)] = a;
258 if ((strlen(wordbuf) + 3) > screenwidth) {
260 for (b = 0; b < strlen(wordbuf); ++b)
261 if (wordbuf[b] == 32)
263 if (last_space >= 0) {
264 for (b = 0; b < strlen(wordbuf); ++b)
265 if (wordbuf[b] == 32) {
267 add_word(textlist, wordbuf);
268 strcpy(wordbuf, &wordbuf[b + 1]);
271 for (b = 0; b < strlen(wordbuf); ++b) {
276 scr_printf("\n%s", wordbuf);
279 add_word(textlist, wordbuf);
285 } while (finished == 0);
287 // write the buffer back to disk
289 for (ptr = textlist; ptr != NULL; ptr = ptr->next) {
290 fprintf(fp, "%s", ptr->text);
294 rv = ftruncate(fileno(fp), ftell(fp));
296 scr_printf("failed to set message buffer: %s\n", strerror(errno));
299 // and deallocate the memory we used
300 while (textlist != NULL) {
301 ptr = textlist->next;
308 // Free the struct parts
309 void free_parts(struct parts *p) {
310 struct parts *a_part = p;
316 a_part = a_part->next;
322 // This is a mini RFC2047 decoder.
323 // It only handles strings encoded from UTF-8 as Quoted-printable.
324 // We can do this "in place" because the converted string will always be smaller than the source string.
325 void mini_2047_decode(char *s) {
326 if (!s) { // no null strings allowed!
330 char *qstart = strstr(s, "=?UTF-8?Q?"); // Must start with this string
335 char *qend = strstr(qstart + 10, "?="); // Must end with this string
340 if (qend <= qstart) { // And there must be something in between them.
344 // The string has qualified for conversion.
346 strcpy(qend, ""); // Strip the trailer
347 strcpy(qstart, &qstart[10]); // Strip the header
349 char *r = qstart; // Pointer to where in the string we're reading
350 char *w = s; // Pointer to where in the string we're writing
352 while (*r) { // Loop through the source string
353 if (r[0] == '=') { // "=" means read a hex character
359 sscanf(ch, "%02x", &c);
364 else if (r[0] == '_') { // "_" is a space
369 else { // anything else pass through literally
375 w[0] = 0; // null terminate
379 // Read a message from the server
380 int read_message(CtdlIPC *ipc,
381 long num, // message number
382 int pagin, // 0 = normal read, 1 = read with pagination, 2 = header
383 FILE *dest // Destination file, NULL for screen
390 struct ctdlipcmessage *message = NULL;
391 int r; // IPC response code
392 char *converted_text = NULL;
399 int final_line_is_blank = 0;
405 strcpy(reply_to, NO_REPLY_TO);
406 strcpy(reply_subject, "");
407 strcpy(reply_references, "");
408 strcpy(reply_inreplyto, "");
410 r = CtdlIPCGetSingleMessage(ipc, num, (pagin == READ_HEADER ? 1 : 0), 4, &message, buf);
412 scr_printf("*** msg #%ld: %d %s\n", num, r, buf);
415 free_parts(message->attachments);
421 fprintf(dest, "\n ");
429 if (pagin == 1 && !dest) {
435 scr_printf("nhdr=%s\nfrom=%s\ntype=%d\nmsgn=%s\n",
436 message->nhdr ? "yes" : "no", message->author, message->type, message->msgid);
437 if (!IsEmptyStr(message->subject)) {
438 scr_printf("subj=%s\n", message->subject);
440 if (!IsEmptyStr(message->email)) {
441 scr_printf("rfca=%s\n", message->email);
443 scr_printf("room=%s\ntime=%s", message->room, asctime(localtime(&message->time)));
444 if (!IsEmptyStr(message->recipient)) {
445 scr_printf("rcpt=%s\n", message->recipient);
447 if (message->attachments) {
450 for (ptr = message->attachments; ptr; ptr = ptr->next) {
451 scr_printf("part=%s|%s|%s|%s|%s|%ld\n",
452 ptr->name, ptr->filename, ptr->number, ptr->disposition, ptr->mimetype, ptr->length);
458 free_parts(message->attachments);
463 if (rc_display_message_numbers) {
465 fprintf(dest, "[#%s] ", message->msgid);
471 scr_printf("#%s", message->msgid);
476 if (nhdr == 1 && !is_room_aide) {
478 fprintf(dest, " ****");
486 localtime_r(&message->time, &thetime);
487 strftime(now, sizeof now, "%F %R", &thetime);
489 fprintf(dest, "%s from %s ", now, message->author);
490 if (!message->is_local) {
491 fprintf(dest, "<%s> ", message->email);
496 scr_printf("%s ", now);
500 scr_printf("%s ", message->author);
501 if (!message->is_local) {
505 scr_printf("%s", message->email);
510 if (strcasecmp(message->room, room_name) && (IsEmptyStr(message->email))) {
512 fprintf(dest, "in %s> ", message->room);
517 color(BRIGHT_MAGENTA);
518 scr_printf("%s> ", message->room);
521 if (!IsEmptyStr(message->recipient)) {
523 fprintf(dest, "to %s ", message->recipient);
529 scr_printf("%s ", message->recipient);
541 // Set the reply-to address to an Internet e-mail address if possible
542 if ((message->email != NULL) && (!IsEmptyStr(message->email))) {
543 if (!IsEmptyStr(message->author)) {
544 snprintf(reply_to, sizeof reply_to, "%s <%s>", message->author, message->email);
547 strncpy(reply_to, message->email, sizeof reply_to);
551 // But if we can't do that, set it to a Citadel address.
552 if (!strcmp(reply_to, NO_REPLY_TO)) {
553 strncpy(reply_to, message->author, sizeof(reply_to));
556 if (message->msgid != NULL) {
557 strncpy(reply_inreplyto, message->msgid, sizeof reply_inreplyto);
560 if (message->references != NULL) {
561 if (!IsEmptyStr(message->references)) {
562 strncpy(reply_references, message->references, sizeof reply_references);
566 if (message->subject != NULL) {
567 strncpy(reply_subject, message->subject, sizeof reply_subject);
568 if (!IsEmptyStr(message->subject)) {
570 fprintf(dest, "Subject: %s\n", message->subject);
574 scr_printf("Subject: ");
576 mini_2047_decode(message->subject);
577 scr_printf("%s\n", message->subject);
583 if (pagin == 1 && !dest) {
587 // ****** end of header output, start of message text output ******
589 // Convert HTML to plain text, formatting for the actual width of the client screen.
590 if (!strcasecmp(message->content_type, "text/html")) {
591 converted_text = html_to_ascii(message->text, 0, screenwidth,
592 ((enable_color ? H2A_ANSI : 0) | (rc_sixel ? H2A_SIXEL : 0))
594 if (converted_text != NULL) {
596 message->text = converted_text;
601 // Text/plain is a different type
602 if (!strcasecmp(message->content_type, "text/plain")) {
606 // Render text/x-markdown as plain text
607 if (!strcasecmp(message->content_type, "text/x-markdown")) {
612 static char *urlprefixes[] = {
618 num_urls = 0; // Start with a clean slate
619 for (p = 0; p < (sizeof urlprefixes / sizeof(char *)); ++p) {
620 searchptr = message->text;
621 while ((searchptr != NULL) && (num_urls < MAXURLS)) {
622 searchptr = strstr(searchptr, urlprefixes[p]);
623 if (searchptr != NULL) {
624 strncpy(urls[num_urls], searchptr, sizeof(urls[num_urls]));
625 for (i = 0; i < strlen(urls[num_urls]); i++) {
626 ch = urls[num_urls][i];
627 if (ch == '>' || ch == '\"' || ch == ')' || ch == ' ' || ch == '\n') {
628 urls[num_urls][i] = 0;
639 if (format_type == 0) {
640 // renderer for legacy Citadel format
641 fr = fmout(screenwidth, NULL, message->text, dest, 1);
644 // renderer for text/plain
645 lineptr = message->text;
647 nextline = strchr(lineptr, '\n');
648 if (nextline != NULL) {
655 if (sigcaught == 0) {
656 linelen = strlen(lineptr);
657 if (linelen && (lineptr[linelen - 1] == '\r')) {
658 lineptr[--linelen] = 0;
661 fprintf(dest, "%s\n", lineptr);
664 scr_printf("%s\n", lineptr);
668 final_line_is_blank = 1;
670 final_line_is_blank = 0;
675 if (!final_line_is_blank) {
685 // Enumerate any attachments
686 if ((pagin == 1) && (message->attachments)) {
689 for (ptr = message->attachments; ptr; ptr = ptr->next) {
690 if ( (!strcasecmp(ptr->disposition, "attachment"))
691 || (!strcasecmp(ptr->disposition, "inline"))
692 || (!strcasecmp(ptr->disposition, ""))
694 if ( (strcasecmp(ptr->number, message->mime_chosen))
695 && (!IsEmptyStr(ptr->mimetype))
699 color(BRIGHT_MAGENTA);
700 scr_printf("%s", ptr->number);
704 scr_printf("%s", ptr->filename);
706 scr_printf(" (%s, %ld bytes)\n", ptr->mimetype, ptr->length);
707 if (!strncmp(ptr->mimetype, "image/", 6)) {
715 // Save the attachments info for later
716 last_message_parts = message->attachments;
722 if (pagin == 1 && !dest) {
730 // replace string function for the built-in editor
731 void replace_string(char *filename, long int startpos) {
739 int substitutions = 0;
743 newprompt("Enter text to be replaced: ", srch_str, (sizeof(srch_str) - 1));
744 if (IsEmptyStr(srch_str)) {
748 newprompt("Enter text to replace it with: ", rplc_str, (sizeof(rplc_str) - 1));
750 fp = fopen(filename, "r+");
756 fseek(fp, startpos, 0);
758 while (a = getc(fp), a > 0) {
760 buf[strlen(buf) + 1] = 0;
761 buf[strlen(buf)] = a;
762 if (strlen(buf) >= strlen(srch_str)) {
763 ptr = (&buf[strlen(buf) - strlen(srch_str)]);
764 if (!strncmp(ptr, srch_str, strlen(srch_str))) {
765 strcpy(ptr, rplc_str);
769 if (strlen(buf) > 384) {
772 rv = fwrite((char *) buf, 128, 1, fp);
774 scr_printf("failed to replace string: %s\n", strerror(errno));
775 break; // No replacement happened; break out of the loop
777 strcpy(buf, &buf[128]);
783 if (!IsEmptyStr(buf)) {
784 rv = fwrite((char *) buf, strlen(buf), 1, fp);
788 rv = truncate(filename, wpos);
789 scr_printf("<R>eplace made %d substitution(s).\n\n", substitutions);
793 // Function to begin composing a new message
794 int client_make_message(CtdlIPC *ipc,
795 char *filename, // temporary file name
796 char *recipient, // NULL if it's not mail
800 char *subject, // buffer to store subject line
810 if ((mode == 2) && (IsEmptyStr(editor_path))) {
811 scr_printf("*** No editor available; using built-in editor.\n");
816 time_t now = time(NULL);
817 localtime_r(&now, &thetime);
818 strftime(datestr, sizeof datestr, "%F %R", &thetime);
821 if (room_flags & QR_ANONONLY && !recipient) {
822 snprintf(header, sizeof header, " ****");
825 snprintf(header, sizeof header, " %s from %s", datestr, (is_anonymous ? "[anonymous]" : fullname));
826 if (!IsEmptyStr(recipient)) {
827 size_t tmp = strlen(header);
828 snprintf(&header[tmp], sizeof header - tmp, " to %s", recipient);
831 scr_printf("%s\n", header);
833 if (!IsEmptyStr(subject)) {
834 scr_printf("Subject: %s\n", subject);
837 if ((subject_required) && (IsEmptyStr(subject))) {
838 newprompt("Subject: ", subject, 70);
842 scr_printf("(Press ctrl-d when finished)\n");
846 fp = fopen(filename, "r");
848 fmout(screenwidth, fp, NULL, NULL, 0);
851 scr_printf("failed to get stream position %s\n", strerror(errno));
855 fp = fopen(filename, "w");
857 scr_printf("*** Error opening temp file!\n %s: %s\n", filename, strerror(errno));
867 fp = fopen(filename, "r+");
869 scr_printf("*** Error opening temp file!\n %s: %s\n", filename, strerror(errno)
878 fp = fopen(filename, "a");
880 scr_printf("*** Error opening temp file!\n" " %s: %s\n", filename, strerror(errno));
900 default: // allow 2+ modes
901 e_ex_code = 1; // start with a failed exit code
902 stty_ctdl(SB_RESTORE);
904 cksum = file_checksum(filename);
905 if (editor_pid == 0) {
908 chmod(filename, 0600);
909 snprintf(tmp, sizeof tmp, "WINDOW_TITLE=%s", header);
911 execlp(editor_path, editor_path, filename, NULL);
917 b = ka_wait(&e_ex_code);
918 } while ((b != editor_pid) && (b >= 0));
924 MECR:if (mode >= 2) {
925 if (file_checksum(filename) == cksum) {
926 scr_printf("*** Aborted message.\n");
929 if (e_ex_code == 0) {
935 b = keymenu("Entry command (? for options)",
937 "<C>ontinue|" "<S>ave message|" "<P>rint formatted|" "add s<U>bject|" "<R>eplace string|" "<H>old message");
946 scr_printf(" %s from %s", datestr, fullname);
947 if (!IsEmptyStr(recipient)) {
948 scr_printf(" to %s", recipient);
952 if (!IsEmptyStr(subject)) {
953 scr_printf("Subject: %s\n", subject);
955 fp = fopen(filename, "r");
957 fmout(screenwidth, fp, NULL, NULL, 0);
960 scr_printf("failed to get stream position %s\n", strerror(errno));
966 replace_string(filename, 0L);
973 if (subject != NULL) {
974 newprompt("Subject: ", subject, 70);
981 MEABT:scr_printf("Are you sure? ");
985 MEABT2:unlink(filename);
990 // Make sure there's room in msg_arr[] for at least one more.
991 void check_msg_arr_size(void) {
992 if ((num_msgs + 1) > msg_arr_size) {
994 msg_arr = realloc(msg_arr, ((sizeof(long)) * msg_arr_size));
999 // break_big_lines() - break up lines that are >1024 characters, otherwise the server will truncate them.
1000 void break_big_lines(char *msg) {
1009 while (strlen(ptr) > 1000) {
1010 break_here = strchr(&ptr[900], ' ');
1011 if ((break_here == NULL) || (break_here > &ptr[999])) {
1012 break_here = &ptr[999];
1020 // entmsg() - edit and create a message
1021 // returns 0 if message was saved
1022 int entmsg(CtdlIPC *ipc,
1023 int is_reply, // nonzero if this was a <R>eply command
1025 int masquerade // prompt for a non-default display name?
1034 struct ctdlipcmessage message;
1035 unsigned long *msgarr = NULL;
1036 int r; // IPC response code
1037 int subject_required = 0;
1039 // First, check to see if we have permission to enter a message in
1040 // this room. The server will return an error code if we can't.
1041 if (entmsg_ok == ENTMSG_OK_YES) {
1042 // no problem, go right ahead
1044 else if (entmsg_ok == ENTMSG_OK_BLOG) {
1046 scr_printf("WARNING: this is a BLOG room.\n");
1047 scr_printf("The '<E>nter Message' command will create a BLOG POST.\n");
1048 scr_printf("If you want to leave a comment or reply to a comment, use the '<R>eply' command.\n");
1049 scr_printf("Do you really want to create a new blog post? ");
1056 scr_printf("You may not enter messages in this type of room.\n");
1067 strcpy(subject, "");
1069 strcpy(message.recipient, "");
1070 strcpy(message.author, "");
1071 strcpy(message.subject, "");
1072 strcpy(message.references, "");
1073 message.text = ""; // point to "", changes later
1074 message.anonymous = 0;
1075 message.type = mode;
1078 newprompt("Display name for this message: ", message.author, 40);
1083 if (!IsEmptyStr(reply_subject)) {
1084 if (!strncasecmp(reply_subject, "Re: ", 3)) {
1085 strcpy(message.subject, reply_subject);
1088 snprintf(message.subject, sizeof message.subject, "Re: %s", reply_subject);
1092 // Trim down excessively long lists of thread references. We eliminate the
1093 // second one in the list so that the thread root remains intact.
1094 int rrtok = num_tokens(reply_references, '|');
1095 int rrlen = strlen(reply_references);
1096 if (((rrtok >= 3) && (rrlen > 900)) || (rrtok > 10)) {
1097 remove_token(reply_references, 1, '|');
1100 snprintf(message.references, sizeof message.references, "%s%s%s",
1101 reply_references, (IsEmptyStr(reply_references) ? "" : "|"), reply_inreplyto
1105 r = CtdlIPCPostMessage(ipc, 0, &subject_required, &message, buf);
1107 if (r / 100 != 2 && r / 10 != 57) {
1108 scr_printf("%s\n", buf);
1112 // Error code 570 is special. It means that we CAN enter a message in this room, but a recipient needs to be specified.
1118 // If the user is a dumbass, tell them how to type.
1119 if ((userflags & US_EXPERT) == 0) {
1120 scr_printf("Entering message. Word wrap will give you soft linebreaks. Pressing the\n");
1121 scr_printf("'enter' key will give you a hard linebreak and an indent. Press 'enter' twice\n");
1122 scr_printf("when finished.\n");
1125 // Handle the selection of a recipient, if necessary.
1127 if (need_recp == 1) {
1128 if (axlevel >= AxProbU) {
1130 strcpy(buf, reply_to);
1133 newprompt("Enter recipient: ", buf, SIZ - 100);
1134 if (IsEmptyStr(buf)) {
1140 strcpy(buf, "sysop");
1143 strcpy(message.recipient, buf);
1145 if (room_flags & QR_ANONOPT) {
1146 scr_printf("Anonymous (Y/N)? ");
1148 message.anonymous = 1;
1151 // If it's mail, we've got to check the validity of the recipient...
1152 if (!IsEmptyStr(message.recipient)) {
1153 r = CtdlIPCPostMessage(ipc, 0, &subject_required, &message, buf);
1155 scr_printf("%s\n", buf);
1160 // Learn the number of the newest message in in the room, so we can tell upon saving whether someone else has posted too.
1162 r = CtdlIPCGetMessages(ipc, LastMessages, 1, NULL, &msgarr, buf);
1164 scr_printf("%s\n", buf);
1167 for (num_msgs = 0; msgarr[num_msgs]; num_msgs++);
1170 // Now compose the message...
1171 if (client_make_message(ipc, temp, message.recipient, message.anonymous, 0, c, message.subject, subject_required) != 0) {
1177 // Reopen the temp file that was created, so we can send it
1178 fp = fopen(temp, "r");
1180 if (!fp || !(message.text = load_message_from_file(fp))) {
1181 scr_printf("*** Internal error while trying to save message!\n" "%s: %s\n", temp, strerror(errno));
1189 // Break lines that are >1024 characters, otherwise the server will truncate them.
1190 break_big_lines(message.text);
1192 // Transmit message to the server
1193 r = CtdlIPCPostMessage(ipc, 1, NULL, &message, buf);
1195 scr_printf("%s\n", buf);
1199 // Yes, unlink it now, so it doesn't stick around if we crash
1203 highmsg = msgarr[num_msgs - 1];
1208 r = CtdlIPCGetMessages(ipc, NewMessages, 0, NULL, &msgarr, buf);
1210 scr_printf("%s\n", buf);
1213 for (num_msgs = 0; msgarr[num_msgs]; num_msgs++);
1216 // get new highest message number in room to set lrp for goto...
1217 maxmsgnum = msgarr[num_msgs - 1];
1219 // now see if anyone else has posted in here
1221 for (a = 0; a < num_msgs; ++a) {
1222 if (msgarr[a] > highmsg) {
1231 // In the Mail> room, this algorithm always counts one message higher than in public rooms, so we decrement it by one.
1237 scr_printf("*** 1 additional message has been entered in this room by another user.\n");
1240 scr_printf("*** %d additional messages have been entered in this room by other users.\n", b);
1247 // Do editing on a quoted file
1248 void process_quote(void) {
1249 FILE *qfile, *tfile;
1251 int line, qstart, qend;
1253 // Unlink the second temp file as soon as it's opened, so it'll get deleted even if the program dies
1254 qfile = fopen(temp2, "r");
1257 // Display the quotable text with line numbers added
1259 if (fgets(buf, 128, qfile) == NULL) {
1260 // we're skipping a line here
1262 while (fgets(buf, 128, qfile) != NULL) {
1263 scr_printf("%3d %s", ++line, buf);
1266 qstart = intprompt("Begin quoting at", 1, 1, line);
1267 qend = intprompt(" End quoting at", line, qstart, line);
1271 if (fgets(buf, 128, qfile) == NULL) {
1272 // we're skipping a line here
1274 tfile = fopen(temp, "w");
1275 while (fgets(buf, 128, qfile) != NULL) {
1276 if ((++line >= qstart) && (line <= qend)) {
1277 fprintf(tfile, " >%s", buf);
1280 fprintf(tfile, " \n");
1287 // List the URLs which were embedded in the previous message
1288 void list_urls(CtdlIPC * ipc) {
1293 if (num_urls == 0) {
1294 scr_printf("There were no URLs in the previous message.\n\n");
1298 for (i = 0; i < num_urls; ++i) {
1299 scr_printf("%3d %s\n", i + 1, urls[i]);
1302 if ((i = num_urls) != 1) {
1303 i = intprompt("Display which one", 1, 1, num_urls);
1306 snprintf(cmd, sizeof cmd, rc_url_cmd, urls[i - 1]);
1312 // Run image viewer in background
1313 int do_image_view(const char *filename) {
1317 snprintf(cmd, sizeof cmd, imagecmd, filename);
1324 if (childpid == 0) {
1326 pid_t grandchildpid;
1328 grandchildpid = fork();
1329 if (grandchildpid < 0) {
1330 return grandchildpid;
1333 if (grandchildpid == 0) {
1338 nullfd = open("/dev/null", O_WRONLY);
1345 retcode = system(cmd);
1355 if (grandchildpid > 0) {
1363 waitpid(childpid, &retcode, 0);
1371 // View an image attached to a message
1372 void image_view(CtdlIPC * ipc, unsigned long msg) {
1373 struct parts *ptr = last_message_parts;
1377 // Run through available parts
1378 for (ptr = last_message_parts; ptr; ptr = ptr->next) {
1379 if ( (!strcasecmp(ptr->disposition, "attachment")
1380 || !strcasecmp(ptr->disposition, "inline"))
1381 && !strncmp(ptr->mimetype, "image/", 6)
1385 strcpy(part, ptr->number);
1392 strprompt("View which part (0 when done)", part, SIZ - 1);
1395 for (ptr = last_message_parts; ptr; ptr = ptr->next) {
1396 if ((!strcasecmp(ptr->disposition, "attachment")
1397 || !strcasecmp(ptr->disposition, "inline"))
1398 && !strncmp(ptr->mimetype, "image/", 6)
1399 && !strcasecmp(ptr->number, part)) {
1402 void *file = NULL; // The downloaded file
1407 r = CtdlIPCAttachmentDownload(ipc, msg, ptr->number, &file, progress, buf);
1409 scr_printf("%s\n", buf);
1414 len = (size_t) extract_long(buf, 0);
1415 progress(ipc, len, len);
1417 CtdlMakeTempFileName(tmp, sizeof tmp);
1418 strcat(tmp, ptr->filename);
1419 save_buffer(file, len, tmp);
1432 // Read the messages in the current room
1433 void readmsgs(CtdlIPC *ipc,
1434 enum MessageList c, // see listing in citadel_ipc.h
1435 enum MessageDirection rdir, // 1=Forward (-1)=Reverse
1436 int q // Number of msgs to read (if c==3)
1438 int a, e, f, g, start;
1444 char prtfile[PATH_MAX];
1447 char targ[ROOMNAMELEN];
1448 char filename[PATH_MAX];
1449 char save_to[PATH_MAX];
1450 void *attachment = NULL; // Downloaded attachment
1451 FILE *dest = NULL; // Alternate destination other than screen
1452 int r; // IPC response code
1453 static int att_seq = 0; // Attachment download sequence number
1454 int rv = 0; // silence the stupid warn_unused_result warnings
1456 CtdlMakeTempFileName(prtfile, sizeof prtfile);
1462 r = CtdlIPCGetMessages(ipc, c, q, NULL, &msg_arr, cmd);
1464 scr_printf("%s\n", cmd);
1467 for (num_msgs = 0; msg_arr[num_msgs]; num_msgs++);
1470 if (num_msgs == 0) {
1471 if (c == LastMessages) {
1474 scr_printf("*** There are no ");
1475 if (c == NewMessages)
1477 if (c == OldMessages)
1479 scr_printf("messages in this room.\n");
1483 // this loop cycles through each message...
1484 start = ((rdir == 1) ? 0 : (num_msgs - 1));
1485 for (a = start; ((a < num_msgs) && (a >= 0)); a = a + rdir) {
1486 while (msg_arr[a] == 0L) {
1488 if ((a == num_msgs) || (a == (-1)))
1492 RAGAIN:pagin = ((arcflag == 0)
1494 && (userflags & US_PAGINATOR)) ? 1 : 0;
1496 // If we're doing a quote, set the screenwidth to 72
1498 hold_sw = screenwidth;
1502 // If printing or archiving, set the screenwidth to 80
1504 hold_sw = screenwidth;
1509 free_parts(last_message_parts);
1510 last_message_parts = NULL;
1512 // now read the message...
1513 e = read_message(ipc, msg_arr[a], pagin, dest);
1515 // ...and set the screenwidth back if we have to
1516 if ((quotflag) || (arcflag)) {
1517 screenwidth = hold_sw;
1520 highest_msg_read = msg_arr[a];
1525 enable_color = hold_color;
1534 enable_color = hold_color;
1537 if (freopen(prtfile, "r", stdin) == NULL) {
1538 // we probably should handle the error condition here
1540 stty_ctdl(SB_RESTORE);
1541 ka_system(printcmd);
1542 stty_ctdl(SB_NO_INTR);
1549 } while ((g != f) && (g >= 0));
1550 scr_printf("Message printed.\n");
1554 if (((userflags & US_NOPROMPT) || (e == SIGINT))
1555 && (((room_flags & QR_MAILBOX) == 0)
1556 || (rc_force_mail_prompts == 0))) {
1562 color(BRIGHT_WHITE);
1563 scr_printf("%d", num_msgs - a - 1);
1567 keyopt("<B>ack <A>gain <R>eply reply<Q>uoted <N>ext <S>top ");
1568 if (rc_url_cmd[0] && num_urls)
1569 keyopt("<U>RLview ");
1570 if (has_images > 0 && !IsEmptyStr(imagecmd))
1571 keyopt("<I>mages ");
1572 keyopt("<?>help -> ");
1575 e = (inkey() & 127);
1578 if (e == 10) { // return key same as <N>
1582 if (e == 32) { // space key same as <N>
1586 if ( (!is_room_aide) // delete/move are available only to admins
1587 && ((room_flags & QR_MAILBOX) == 0)
1588 && ((room_flags2 & QR2_COLLABDEL) == 0)
1590 if ((e == 'd') || (e == 'm')) {
1595 if ((e == 'p') && (IsEmptyStr(printcmd))) { // print, if available
1599 if ((e == 'f') && (rc_allow_attachments == 0)) { // file attachments, if available
1603 if ((e == 'u') && (IsEmptyStr(rc_url_cmd))) { // display urls, if a browser is available
1607 if ((e == 'i') && (IsEmptyStr(imagecmd) || !has_images)) { // display images, if available
1611 } while ((e != 'a') && (e != 'n') && (e != 's')
1612 && (e != 'd') && (e != 'm') && (e != 'p')
1613 && (e != 'q') && (e != 'b') && (e != 'h')
1614 && (e != 'r') && (e != 'f') && (e != '?')
1615 && (e != 'u') && (e != 'c') && (e != 'y')
1616 && (e != 'i') && (e != 'o')
1623 scr_printf("Again");
1626 scr_printf("Delete");
1638 scr_printf("Print");
1641 scr_printf("reply Quoted");
1647 scr_printf("Header");
1650 scr_printf("Reply");
1653 scr_printf("Open attachments");
1659 scr_printf("URL's");
1662 scr_printf("mY next");
1667 scr_printf("? <help>");
1670 if (userflags & US_DISAPPEAR || e == 'i')
1671 scr_printf("\r%79s\r", "");
1675 DONE_QUOTING: switch (e) {
1677 scr_printf("Options available here:\n"
1678 " ? Help (prints this message)\n"
1679 " S Stop reading immediately\n"
1680 " A Again (repeats last message)\n"
1681 " N Next (continue with next message)\n"
1682 " Y My Next (continue with next message you authored)\n"
1683 " B Back (go back to previous message)\n");
1684 if ((is_room_aide) || (room_flags & QR_MAILBOX) || (room_flags2 & QR2_COLLABDEL)) {
1685 scr_printf(" D Delete this message\n" " M Move message to another room\n");
1687 scr_printf(" C Copy message to another room\n");
1688 if (!IsEmptyStr(printcmd))
1689 scr_printf(" P Print this message\n");
1690 scr_printf(" Q Reply to this message, quoting portions of it\n"
1691 " H Headers (display message headers only)\n");
1693 scr_printf(" R Reply to this message\n");
1694 if (rc_allow_attachments) {
1695 scr_printf(" O (Open attachments)\n");
1696 scr_printf(" F (save attachments to a File)\n");
1698 if (!IsEmptyStr(rc_url_cmd))
1699 scr_printf(" U (list URL's for display)\n");
1700 if (!IsEmptyStr(imagecmd) && has_images > 0)
1701 scr_printf(" I Image viewer\n");
1706 dest = fopen(prtfile, "w");
1708 hold_color = enable_color;
1713 dest = fopen(temp2, "w");
1715 hold_color = enable_color;
1727 newprompt("Enter target room: ", targ, ROOMNAMELEN - 1);
1728 if (!IsEmptyStr(targ)) {
1729 r = CtdlIPCMoveMessage(ipc, (e == 'c' ? 1 : 0), msg_arr[a], targ, cmd);
1730 scr_printf("%s\n", cmd);
1737 if (r / 100 != 2) { // r will be initialized. The logic here sucks.
1743 newprompt("Which section? ", filename, ((sizeof filename) - 1));
1744 r = CtdlIPCAttachmentDownload(ipc, msg_arr[a], filename, &attachment, progress, cmd);
1746 scr_printf("%s\n", cmd);
1749 extract_token(filename, cmd, 2, '|', sizeof filename);
1750 // Part 1 won't have a filename; use the subject of the message instead. --IO
1751 if (IsEmptyStr(filename)) {
1752 strcpy(filename, reply_subject);
1754 if (e == 'o') { // open attachment
1755 mkdir(tempdir, 0700);
1756 snprintf(save_to, sizeof save_to, "%s/%04x.%s", tempdir, ++att_seq, filename);
1757 save_buffer(attachment, extract_unsigned_long(cmd, 0), save_to);
1758 snprintf(cmd, sizeof cmd, rc_open_cmd, save_to);
1761 scr_printf("failed to save %s Reason %d\n", cmd, rv);
1764 else { // save attachment to disk
1765 destination_directory(save_to, filename);
1766 save_buffer(attachment, extract_unsigned_long(cmd, 0), save_to);
1775 scr_printf("*** Delete this message? ");
1777 r = CtdlIPCDeleteMessage(ipc, msg_arr[a], cmd);
1778 scr_printf("%s\n", cmd);
1787 read_message(ipc, msg_arr[a], READ_HEADER, NULL);
1790 savedpos = num_msgs;
1791 entmsg(ipc, 1, ((userflags & US_EXTEDIT) ? 2 : 0), 0);
1792 num_msgs = savedpos;
1798 image_view(ipc, msg_arr[a]);
1801 { /* hack hack hack */
1802 /* find the next message by me, stay here if we find nothing */
1805 for (finda = (a + rdir); ((finda < num_msgs) && (finda >= 0)); finda += rdir) {
1806 /* This is repetitively dumb, but that's what computers are for.
1807 We have to load up messages until we find one by us */
1810 struct ctdlipcmessage *msg = NULL;
1812 /* read the header so we can get 'from=' */
1813 r = CtdlIPCGetSingleMessage(ipc, msg_arr[finda], 1, 0, &msg, buf);
1814 if (!strncasecmp(msg->author, fullname, sizeof(fullname))) {
1815 a = lasta; /* meesa current */
1823 lasta = finda; /* keep one behind or we skip on the reentrance to the for */
1827 } /* end for loop */
1828 } /* end read routine */
1831 // View and edit a system message
1832 void edit_system_message(CtdlIPC * ipc, char *which_message) {
1835 char write_cmd[SIZ];
1837 snprintf(desc, sizeof desc, "system message '%s'", which_message);
1838 snprintf(read_cmd, sizeof read_cmd, "MESG %s", which_message);
1839 snprintf(write_cmd, sizeof write_cmd, "EMSG %s", which_message);
1840 do_edit(ipc, desc, read_cmd, "NOOP", write_cmd);
1844 // Loads the contents of a file into memory. Caller must free the allocated memory.
1845 char *load_message_from_file(FILE *src) {
1850 fseek(src, 0, SEEK_END);
1854 dest = (char *) calloc(1, i + 1);
1862 g = fread(dest + got, 1, i - got, src);
1865 if (errno == EINTR) {
1866 continue; // Interrupted system call, keep going
1868 // At this point we have either EOF or error