AmendHub

Download

jcs

/

subtext

/

mail.c

 

(View History)

jcs   mail: constify fields Latest amendment: 277 on 2022-11-11

1 /*
2 * Copyright (c) 2022 joshua stein <jcs@jcs.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #include <stdarg.h>
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21
22 #include "ansi.h"
23 #include "mail.h"
24 #include "subtext.h"
25 #include "session.h"
26 #include "user.h"
27 #include "uthread.h"
28 #include "util.h"
29
30 #define MSGS_PER_PAGE 10
31
32 #define MAIL_READ_RETURN_DONE -1
33 #define MAIL_READ_RETURN_LIST -2
34 #define MAIL_READ_RETURN_FIND -3
35
36 static const struct bile_object_field mail_object_fields[] = {
37 { offsetof(struct mail_message, recipient_user_id),
38 member_size(struct mail_message, recipient_user_id), -1 },
39 { offsetof(struct mail_message, id),
40 member_size(struct mail_message, id), -1 },
41 { offsetof(struct mail_message, time),
42 member_size(struct mail_message, time), -1 },
43 { offsetof(struct mail_message, read),
44 member_size(struct mail_message, read), -1 },
45 { offsetof(struct mail_message, sender_user_id),
46 member_size(struct mail_message, sender_user_id), -1 },
47 { offsetof(struct mail_message, subject_size),
48 member_size(struct mail_message, subject_size), -1 },
49 { offsetof(struct mail_message, subject),
50 -1, offsetof(struct mail_message, subject_size) },
51 { offsetof(struct mail_message, body_size),
52 member_size(struct mail_message, body_size), -1 },
53 { offsetof(struct mail_message, body),
54 -1, offsetof(struct mail_message, body_size) },
55 { offsetof(struct mail_message, parent_message_id),
56 member_size(struct mail_message, parent_message_id), -1 },
57 };
58 static const size_t nmail_object_fields = nitems(mail_object_fields);
59
60 void mail_free_message_strings(struct mail_message *msg);
61 short mail_save(struct session *s, struct mail_message *msg);
62 short mail_read(struct session *s, unsigned long id, short idx);
63 void mail_list(struct session *s, size_t nmsgs, unsigned long *mail_ids,
64 size_t page, size_t pages);
65 short mail_get_message_id(struct session *s, size_t nmsgs, char *prompt,
66 short initial);
67
68 void
69 mail_free_message_strings(struct mail_message *msg)
70 {
71 if (msg->subject != NULL) {
72 xfree(&msg->subject);
73 msg->subject = NULL;
74 }
75 if (msg->body != NULL) {
76 xfree(&msg->body);
77 msg->body = NULL;
78 }
79 }
80
81 short
82 mail_get_message_id(struct session *s, size_t nmsgs, char *prompt,
83 short initial)
84 {
85 char *tmp = NULL;
86 char start_s[10] = { 0 };
87 short ret;
88
89 if (initial > -1)
90 snprintf(start_s, sizeof(start_s), "%d", initial);
91
92 get_id:
93 if (prompt)
94 session_printf(s, "{{B}}%s:{{/B}} ", prompt);
95
96 tmp = session_field_input(s, 5, 5, start_s, false, 0);
97 session_output(s, "\r\n", 2);
98 session_flush(s);
99 if (tmp == NULL || s->ending) {
100 ret = -1;
101 goto get_id_done;
102 }
103
104 if (sscanf(tmp, "%d", &ret) != 1 || ret < 1 || ret > nmsgs) {
105 session_printf(s, "{{B}}Invalid message ID{{/B}} (^C to cancel)\r\n");
106 session_flush(s);
107 xfree(&tmp);
108 goto get_id;
109 }
110
111 get_id_done:
112 if (tmp != NULL)
113 xfree(&tmp);
114 return ret;
115 }
116
117 void
118 mail_menu(struct session *s)
119 {
120 static const struct session_menu_option opts[] = {
121 { '#', "#0123456789", "Read mail message [#]" },
122 { '<', "<", "Previous page of messages" },
123 { 'l', "Ll", "List mail messages" },
124 { '>', ">", "Next page of messages" },
125 { 'm', "MmNn", "Compose new mail message" },
126 { 'q', "QqXx", "Return to main menu" },
127 { '?', "?", "Show this help menu" },
128 };
129 size_t nmsgs, nmail_ids, id;
130 unsigned long *mail_ids = NULL, page, pages;
131 short ret;
132 char c;
133 bool show_help = false;
134 bool done = false;
135 bool find_message_ids = true;
136 bool show_list = true;
137
138 if (!s->user) {
139 session_printf(s, "Mail is not available to guests.\r\n"
140 "Please create an account first.\r\n");
141 session_flush(s);
142 return;
143 }
144
145 page = 0;
146
147 while (!done && !s->ending) {
148 if (find_message_ids) {
149 if (mail_ids != NULL)
150 xfree(&mail_ids);
151 nmsgs = mail_find_ids_for_user(s->user, &nmail_ids, &mail_ids,
152 page * MSGS_PER_PAGE, MSGS_PER_PAGE, false);
153 /* ceil(nmsgs / MSGS_PER_PAGE) */
154 pages = (nmsgs + MSGS_PER_PAGE - 1) / MSGS_PER_PAGE;
155
156 if (page >= pages)
157 page = pages - 1;
158
159 find_message_ids = false;
160 }
161
162 if (show_list) {
163 mail_list(s, nmail_ids, mail_ids, page + 1, pages);
164 show_list = false;
165 }
166
167 c = session_menu(s, "Private Mail", "Mail", opts, nitems(opts),
168 show_help);
169 show_help = false;
170
171 handle_opt:
172 switch (c) {
173 case 'm':
174 mail_compose(s, NULL, NULL, NULL);
175 break;
176 case 'l':
177 show_list = true;
178 break;
179 case '>':
180 case '<':
181 if (c == '>' && page == pages - 1) {
182 session_printf(s, "You are at the last page of messages\r\n");
183 session_flush(s);
184 break;
185 }
186 if (c == '<' && page == 0) {
187 session_printf(s, "You are already at the first page\r\n");
188 session_flush(s);
189 break;
190 }
191 if (c == '>')
192 page++;
193 else
194 page--;
195 find_message_ids = true;
196 show_list = true;
197 break;
198 case 0:
199 case 1:
200 case 2:
201 case 3:
202 case 4:
203 case 5:
204 case 6:
205 case 7:
206 case 8:
207 case 9:
208 if (c >= nmail_ids) {
209 session_printf(s, "Invalid message\r\n");
210 session_flush(s);
211 break;
212 }
213 ret = mail_read(s, mail_ids[c], c);
214 switch (ret) {
215 case MAIL_READ_RETURN_DONE:
216 break;
217 case MAIL_READ_RETURN_LIST:
218 show_list = true;
219 break;
220 case MAIL_READ_RETURN_FIND:
221 find_message_ids = true;
222 show_list = true;
223 break;
224 default:
225 c = ret;
226 goto handle_opt;
227 }
228 break;
229 case '?':
230 show_help = true;
231 break;
232 default:
233 done = true;
234 break;
235 }
236 }
237
238 if (mail_ids != NULL)
239 xfree(&mail_ids);
240 }
241
242 void
243 mail_compose(struct session *s, char *initial_to, char *initial_subject,
244 char *initial_body)
245 {
246 struct user *to_user = NULL;
247 struct mail_message msg = { 0 };
248 char *to_username = NULL, *tmp;
249 char c;
250
251 if (!s->user) {
252 session_printf(s, "Mail is not available to guests.\r\n"
253 "Please create an account first.\r\n");
254 session_flush(s);
255 return;
256 }
257
258 if (initial_to)
259 to_username = xstrdup(initial_to, "mail_compose");
260 if (initial_subject)
261 msg.subject = xstrdup(initial_subject, "mail_compose");
262 if (initial_body)
263 msg.body = xstrdup(initial_body, "mail_compose");
264
265 session_printf(s, "{{B}}Compose New Private Mail{{/B}}\r\n");
266 session_printf(s, "{{B}}From: {{/B}} %s\r\n", s->user->username);
267 session_flush(s);
268
269 mail_compose_start:
270 for (;;) {
271 session_printf(s, "{{B}}To: {{/B}} ");
272 session_flush(s);
273
274 tmp = session_field_input(s, DB_USERNAME_LENGTH + 1,
275 DB_USERNAME_LENGTH + 1, to_username, false, 0);
276 if (to_username != NULL)
277 xfree(&to_username);
278 to_username = tmp;
279 session_output(s, "\r\n", 2);
280 session_flush(s);
281 if (to_username == NULL || to_username[0] == '\0')
282 goto mail_compose_done;
283
284 to_user = user_find_by_username(to_username);
285 if (to_user == NULL) {
286 session_printf(s, "{{B}}Error:{{/B}}{{#}} No such user \"%s\" "
287 "(^C to cancel)\r\n", to_username);
288 session_flush(s);
289 xfree(&to_username);
290 to_username = NULL;
291 continue;
292 }
293 msg.recipient_user_id = to_user->id;
294 xfree(&to_user);
295 break;
296 }
297
298 for (;;) {
299 session_printf(s, "{{B}}Subject:{{/B}} ");
300 session_flush(s);
301
302 tmp = session_field_input(s, 50, 50, msg.subject, false, 0);
303 if (msg.subject != NULL)
304 xfree(&msg.subject);
305 msg.subject = tmp;
306 session_output(s, "\r\n", 2);
307 session_flush(s);
308
309 if (msg.subject == NULL)
310 goto mail_compose_done;
311
312 rtrim(msg.subject, "\r\n\t ");
313
314 if (msg.subject[0] == '\0') {
315 session_printf(s, "{{B}}Error:{{/B}} Subject cannot "
316 "be blank (^C to cancel)\r\n");
317 session_flush(s);
318 xfree(&msg.subject);
319 msg.subject = NULL;
320 continue;
321 }
322 msg.subject_size = strlen(msg.subject) + 1;
323 break;
324 }
325
326 for (;;) {
327 session_printf(s,
328 "{{B}}Message (^D when finished):{{/B}}\r\n");
329 session_flush(s);
330
331 tmp = session_field_input(s, 2048, s->terminal_columns - 1,
332 msg.body, true, 0);
333 if (msg.body != NULL)
334 xfree(&msg.body);
335 msg.body = tmp;
336 session_output(s, "\r\n", 2);
337 session_flush(s);
338
339 if (msg.body == NULL)
340 goto mail_compose_done;
341
342 rtrim(msg.body, "\r\n\t ");
343
344 if (msg.body[0] == '\0') {
345 session_printf(s, "{{B}}Error:{{/B}} Message cannot "
346 "be blank (^C to cancel)\r\n");
347 session_flush(s);
348 xfree(&msg.body);
349 msg.body = NULL;
350 continue;
351 }
352 msg.body_size = strlen(msg.body) + 1;
353 break;
354 }
355
356 for (;;) {
357 session_printf(s, "\r\n{{B}}(S){{/B}}end message, "
358 "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? ");
359 session_flush(s);
360
361 c = session_input_char(s);
362 if (c == 0 && s->obuflen > 0) {
363 s->node_funcs->output(s);
364 uthread_yield();
365 if (s->ending)
366 goto mail_compose_done;
367 continue;
368 }
369
370 session_printf(s, "%c\r\n", c);
371 session_flush(s);
372
373 switch (c) {
374 case 's':
375 case 'S':
376 case 'y':
377 case '\n':
378 case '\r':
379 /* send */
380 session_printf(s, "Sending mail...");
381 session_flush(s);
382
383 msg.time = Time;
384 msg.sender_user_id = s->user->id;
385 mail_save(s, &msg);
386
387 session_printf(s, " sent!\r\n");
388 session_flush(s);
389
390 goto mail_compose_done;
391 break;
392 case 'e':
393 case 'E':
394 goto mail_compose_start;
395 case 'c':
396 case 'C':
397 case CONTROL_C:
398 goto mail_compose_done;
399 }
400 }
401
402 mail_compose_done:
403 if (to_username != NULL)
404 xfree(&to_username);
405 mail_free_message_strings(&msg);
406 }
407
408 void
409 mail_list(struct session *s, size_t nmail_ids, unsigned long *mail_ids,
410 size_t page, size_t pages)
411 {
412 char time[10];
413 size_t n, size;
414 struct mail_message msg;
415 struct username_cache *user;
416 char *data;
417
418 session_printf(s, "{{B}}Private Mail (Page %ld of %ld){{/B}}\r\n",
419 page, pages);
420 session_printf(s, "%s# Flg Date From Subject%s\r\n",
421 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
422 session_flush(s);
423
424 for (n = 0; n < nmail_ids; n++) {
425 size = bile_read_alloc(db->bile, DB_MESSAGE_RTYPE, mail_ids[n],
426 &data);
427 if (size == 0)
428 break;
429 bile_unmarshall_object(db->bile, mail_object_fields,
430 nitems(mail_object_fields), data, size, &msg, sizeof(msg), true,
431 "mail_list");
432 xfree(&data);
433
434 user = user_username(msg.sender_user_id);
435 strftime(time, sizeof(time), "%b %d", localtime(&msg.time));
436
437 session_printf(s, "%s%ld %c %s %- 10s {{#}}%- 40s%s\r\n",
438 msg.read ? "" : ansi(s, ANSI_BOLD, ANSI_END),
439 n,
440 msg.read ? ' ' : 'N',
441 time,
442 user ? user->username : "(unknown)",
443 msg.subject,
444 msg.read ? "" : ansi(s, ANSI_RESET, ANSI_END));
445
446 mail_free_message_strings(&msg);
447 }
448
449 session_flush(s);
450 }
451
452 short
453 mail_read(struct session *s, unsigned long id, short idx)
454 {
455 static const struct session_menu_option opts[] = {
456 { 'r', "Rr", "Reply to this message" },
457 { 'd', "Dd", "Delete this message" },
458 { 'u', "Uu", "Mark this message unread" },
459 { 'l', "Ll", "Return and list mail messages" },
460 { '#', "#0123456789", "Return and read mail message [#]" },
461 { 'q', "QqXx", "Return to message list" },
462 { '?', "?", "Show this help menu" },
463 };
464 char time[32];
465 char prompt[24];
466 char title[50];
467 size_t size;
468 struct mail_message msg;
469 struct username_cache *sender, *recipient;
470 short ret = MAIL_READ_RETURN_DONE;
471 char *data, *reply_subject;
472 bool done = false, show_help = false;
473 char c;
474
475 size = bile_read_alloc(db->bile, DB_MESSAGE_RTYPE, id, &data);
476 if (size == 0) {
477 session_printf(s, "{{B}}Error:{{/B}} Can't find message\r\n");
478 session_flush(s);
479 return MAIL_READ_RETURN_DONE;
480 }
481
482 bile_unmarshall_object(db->bile, mail_object_fields,
483 nitems(mail_object_fields), data, size, &msg, sizeof(msg), true,
484 "mail_read");
485 xfree(&data);
486
487 sender = user_username(msg.sender_user_id);
488 recipient = user_username(msg.recipient_user_id);
489
490 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
491 localtime(&msg.time));
492
493 session_printf(s, "{{B}}From:{{/B}} %s\r\n",
494 sender ? sender->username : "(unknown)");
495 session_printf(s, "{{B}}To:{{/B}} %s\r\n",
496 recipient ? recipient->username : "(unknown)");
497 session_printf(s, "{{B}}Date:{{/B}} %s %s\r\n", time,
498 db->config.timezone);
499 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s\r\n", msg.subject);
500 session_flush(s);
501 session_printf(s, "\r\n");
502 session_output(s, msg.body, msg.body_size);
503 session_printf(s, "\r\n");
504 session_flush(s);
505
506 if (!msg.read) {
507 msg.read = Time;
508 if (mail_save(s, &msg) != 0)
509 session_printf(s, "Failed marking message read!\r\n");
510 }
511
512 snprintf(prompt, sizeof(prompt), "Mail:Message %d", idx);
513 snprintf(title, sizeof(title), "Private Mail: Message %d", idx);
514
515 while (!done && !s->ending) {
516 c = session_menu(s, title, prompt, opts, nitems(opts),
517 show_help);
518 show_help = false;
519
520 switch (c) {
521 case 'r':
522 if (!sender)
523 break;
524
525 reply_subject = xmalloc(strlen(msg.subject) + 5,
526 "mail_read subject");
527 if (strncmp(msg.subject, "Re:", 3) == 0)
528 strlcpy(reply_subject, msg.subject,
529 strlen(msg.subject) + 1);
530 else
531 sprintf(reply_subject, "Re: %s", msg.subject);
532
533 mail_compose(s, sender->username, reply_subject, NULL);
534
535 xfree(&reply_subject);
536 break;
537 case 'd':
538 if (bile_delete(db->bile, DB_MESSAGE_RTYPE, msg.id) == 0)
539 session_printf(s, "Deleted message {{B}}%d{{/B}}\r\n", idx);
540 else
541 session_printf(s, "Failed deleting message "
542 "{{B}}%d{{/B}}! (%d)\r\n", idx, bile_error(db->bile));
543 ret = MAIL_READ_RETURN_FIND;
544 done = true;
545 break;
546 case 'u':
547 msg.read = 0;
548 if (mail_save(s, &msg) == 0)
549 session_printf(s, "Marked message {{B}}%d{{/B}} unread\r\n",
550 idx);
551 else
552 session_printf(s, "Failed updating message "
553 "{{B}}%d{{/B}}! (%d)\r\n", idx, bile_error(db->bile));
554 ret = MAIL_READ_RETURN_DONE;
555 done = true;
556 break;
557 case 'l':
558 done = true;
559 ret = MAIL_READ_RETURN_LIST;
560 break;
561 case 'q':
562 done = true;
563 ret = MAIL_READ_RETURN_DONE;
564 break;
565 case 0:
566 case 1:
567 case 2:
568 case 3:
569 case 4:
570 case 5:
571 case 6:
572 case 7:
573 case 8:
574 case 9:
575 ret = c;
576 done = true;
577 break;
578 case '?':
579 show_help = true;
580 break;
581 }
582 }
583
584 mail_free_message_strings(&msg);
585
586 return ret;
587 }
588
589 short
590 mail_save(struct session *s, struct mail_message *msg)
591 {
592 size_t size;
593 char *data;
594 short ret;
595
596 if (!msg->id)
597 msg->id = bile_next_id(db->bile, DB_MESSAGE_RTYPE);
598
599 ret = bile_marshall_object(db->bile, mail_object_fields,
600 nitems(mail_object_fields), msg, &data, &size, "mail_save");
601 if (ret != 0 || size == 0) {
602 warn("mail_save: failed to marshall object");
603 return -1;
604 }
605
606 if (bile_write(db->bile, DB_MESSAGE_RTYPE, msg->id, data,
607 size) != size) {
608 warn("mail_save: bile_write failed! %d", bile_error(db->bile));
609 return bile_error(db->bile);
610 }
611
612 return 0;
613 }
614
615 size_t
616 mail_find_ids_for_user(struct user *user, size_t *nmail_ids,
617 unsigned long **mail_ids, size_t offset, size_t limit, bool only_unread)
618 {
619 unsigned long msg_user_id;
620 struct bile_object *o;
621 struct mail_message msg;
622 size_t n, nmsgs_for_user, nret, mail_ids_size, id, size;
623 short i, j;
624 char *data;
625 bool read;
626
627 mail_ids_size = sizeof(long) * 16;
628 if (mail_ids != NULL)
629 *mail_ids = xmalloc(mail_ids_size, "mail_find_ids_for_user ids");
630 if (nmail_ids != NULL)
631 *nmail_ids = 0;
632
633 nmsgs_for_user = 0;
634 for (n = 0; o = bile_get_nth_of_type(db->bile, n, DB_MESSAGE_RTYPE);
635 n++) {
636 id = o->id;
637 bile_read(db->bile, DB_MESSAGE_RTYPE, id, (char *)&msg_user_id,
638 sizeof(msg_user_id));
639 xfree(&o);
640
641 if (msg_user_id != user->id)
642 continue;
643
644 if (only_unread) {
645 size = bile_read_alloc(db->bile, DB_MESSAGE_RTYPE, id, &data);
646 bile_unmarshall_object(db->bile, mail_object_fields,
647 nmail_object_fields, data, size, &msg, sizeof(msg), false,
648 "mail_find_ids_for_user");
649 xfree(&data);
650 read = msg.read;
651 if (read)
652 continue;
653 }
654
655 if (mail_ids != NULL) {
656 EXPAND_TO_FIT(*mail_ids, mail_ids_size,
657 (nmsgs_for_user + 1) * sizeof(long), sizeof(long),
658 sizeof(long) * 16);
659 (*mail_ids)[nmsgs_for_user] = id;
660 }
661
662 nmsgs_for_user++;
663 }
664
665 if (mail_ids != NULL) {
666 /* sort by message id for consistent ordering */
667 for (i = 0; i < nmsgs_for_user; i++) {
668 for (j = 0; j < nmsgs_for_user - i - 1; j++) {
669 if ((*mail_ids)[j] > (*mail_ids)[j + 1]) {
670 id = (*mail_ids)[j];
671 (*mail_ids)[j] = (*mail_ids)[j + 1];
672 (*mail_ids)[j + 1] = id;
673 }
674 }
675 }
676 }
677
678 if (nmail_ids != NULL)
679 *nmail_ids = nmsgs_for_user;
680
681 if (offset) {
682 if (offset >= nmsgs_for_user) {
683 if (nmail_ids != NULL)
684 *nmail_ids = 0;
685 if (mail_ids != NULL)
686 xfree(&(*mail_ids));
687 } else {
688 if (mail_ids != NULL) {
689 for (j = offset, i = 0; j < nmsgs_for_user; j++, i++)
690 (*mail_ids)[i] = (*mail_ids)[j];
691 }
692 if (nmail_ids != NULL)
693 *nmail_ids -= offset;
694 }
695 }
696
697 if (nmail_ids != NULL && *nmail_ids > limit)
698 *nmail_ids = limit;
699
700 return nmsgs_for_user;
701 }