AmendHub

Download

jcs

/

subtext

/

mail.c

 

(View History)

jcs   *: Minor cleanups, remove dead variables, fix some error paths Latest amendment: 580 on 2024-01-24

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 "binkp.h"
24 #include "logger.h"
25 #include "mail.h"
26 #include "subtext.h"
27 #include "session.h"
28 #include "user.h"
29 #include "uthread.h"
30 #include "util.h"
31
32 #define MSGS_PER_PAGE 20
33
34 #define MAIL_READ_RETURN_DONE -1
35 #define MAIL_READ_RETURN_LIST -2
36 #define MAIL_READ_RETURN_FIND -3
37
38 static const struct bile_object_field mail_object_fields[] = {
39 { offsetof(struct mail_message, recipient_user_id),
40 member_size(struct mail_message, recipient_user_id), -1 },
41 { offsetof(struct mail_message, id),
42 member_size(struct mail_message, id), -1 },
43 { offsetof(struct mail_message, time),
44 member_size(struct mail_message, time), -1 },
45 { offsetof(struct mail_message, read),
46 member_size(struct mail_message, read), -1 },
47 { offsetof(struct mail_message, sender_user_id),
48 member_size(struct mail_message, sender_user_id), -1 },
49 { offsetof(struct mail_message, subject_size),
50 member_size(struct mail_message, subject_size), -1 },
51 { offsetof(struct mail_message, subject),
52 -1, offsetof(struct mail_message, subject_size) },
53 { offsetof(struct mail_message, body_size),
54 member_size(struct mail_message, body_size), -1 },
55 { offsetof(struct mail_message, body),
56 -1, offsetof(struct mail_message, body_size) },
57 { offsetof(struct mail_message, parent_message_id),
58 member_size(struct mail_message, parent_message_id), -1 },
59
60 { offsetof(struct mail_message, ftn_msgid),
61 member_size(struct mail_message, ftn_msgid), -1 },
62 { offsetof(struct mail_message, ftn_orig),
63 member_size(struct mail_message, ftn_orig), -1 },
64 { offsetof(struct mail_message, ftn_dest),
65 member_size(struct mail_message, ftn_dest), -1 },
66 { offsetof(struct mail_message, ftn_from),
67 member_size(struct mail_message, ftn_from), -1 },
68 { offsetof(struct mail_message, ftn_to),
69 member_size(struct mail_message, ftn_to), -1 },
70 { offsetof(struct mail_message, ftn_msgid_orig),
71 member_size(struct mail_message, ftn_msgid_orig), -1 },
72 { offsetof(struct mail_message, ftn_reply),
73 member_size(struct mail_message, ftn_reply), -1 },
74 };
75 static const size_t nmail_object_fields = nitems(mail_object_fields);
76
77 void mail_free_message_strings(struct mail_message *msg);
78 short mail_read(struct session *s, unsigned long id, short idx);
79 void mail_list(struct session *s, size_t nmsgs, unsigned long *mail_ids,
80 size_t page, size_t pages);
81 short mail_get_message_id(struct session *s, size_t nmsgs, char *prompt,
82 short initial);
83
84 void
85 mail_free_message_strings(struct mail_message *msg)
86 {
87 if (msg->subject != NULL) {
88 xfree(&msg->subject);
89 msg->subject = NULL;
90 }
91 if (msg->body != NULL) {
92 xfree(&msg->body);
93 msg->body = NULL;
94 }
95 }
96
97 short
98 mail_get_message_id(struct session *s, size_t nmsgs, char *prompt,
99 short initial)
100 {
101 char *tmp = NULL;
102 char start_s[10] = { 0 };
103 short ret;
104
105 if (initial > -1)
106 snprintf(start_s, sizeof(start_s), "%d", initial);
107
108 get_id:
109 if (prompt)
110 session_printf(s, "{{B}}%s:{{/B}} ", prompt);
111
112 tmp = session_field_input(s, 5, 5, start_s, false, 0);
113 session_output(s, "\r\n", 2);
114 session_flush(s);
115 if (tmp == NULL || s->ending) {
116 ret = -1;
117 goto get_id_done;
118 }
119
120 if (sscanf(tmp, "%d", &ret) != 1 || ret < 1 || ret > nmsgs) {
121 session_printf(s, "{{B}}Invalid message ID{{/B}} (^C to cancel)\r\n");
122 session_flush(s);
123 xfree(&tmp);
124 goto get_id;
125 }
126
127 get_id_done:
128 if (tmp != NULL)
129 xfree(&tmp);
130 return ret;
131 }
132
133 void
134 mail_menu(struct session *s)
135 {
136 static const struct session_menu_option opts[] = {
137 { '#', "#", "Read mail message [#]" },
138 { '<', "<", "Newer messages" },
139 { 'l', "Ll", "List mail messages" },
140 { '>', ">", "Older messages" },
141 { 'm', "MmNn", "Compose new mail message" },
142 { 'q', "QqXx", "Return to main menu" },
143 { '?', "?", "Show this help menu" },
144 };
145 static const char prompt_help[] =
146 "#:Read <:Newer >:Older L:List M:Compose Q:Return ?:Help";
147 size_t nmsgs, nmail_ids;
148 unsigned long *mail_ids = NULL, page, pages;
149 short ret, mpp, mn;
150 char c;
151 bool show_help = false;
152 bool done = false;
153 bool find_message_ids = true;
154 bool show_list = true;
155
156 if (!s->user) {
157 session_printf(s, "Mail is not available to guests.\r\n"
158 "Please create an account first.\r\n");
159 session_flush(s);
160 return;
161 }
162
163 page = 0;
164
165 while (!done && !s->ending) {
166 if (find_message_ids) {
167 if (mail_ids != NULL)
168 xfree(&mail_ids);
169 mpp = MSGS_PER_PAGE;
170 if (s->terminal_lines < mpp + 3)
171 mpp = BOUND(mpp, 5, s->terminal_lines - 3);
172 nmsgs = mail_find_ids_for_user(s->user, &nmail_ids, &mail_ids,
173 page * mpp, mpp, false);
174 /* ceil(nmsgs / mpp) */
175 pages = (nmsgs + mpp - 1) / mpp;
176
177 if (page >= pages)
178 page = pages - 1;
179
180 find_message_ids = false;
181 }
182
183 if (show_list) {
184 mail_list(s, nmail_ids, mail_ids, page + 1, pages);
185 show_list = false;
186 }
187
188 c = session_menu(s, "Private Mail", "Mail", (char *)prompt_help,
189 opts, nitems(opts), show_help, "Message #", &mn);
190 show_help = false;
191
192 handle_opt:
193 switch (c) {
194 case 'm':
195 mail_compose(s, NULL, NULL, NULL, NULL);
196 break;
197 case 'l':
198 show_list = true;
199 break;
200 case '>':
201 case '<':
202 if (c == '>' && page == pages - 1) {
203 session_printf(s, "You are at the last page of messages\r\n");
204 session_flush(s);
205 break;
206 }
207 if (c == '<' && page == 0) {
208 session_printf(s, "You are already at the first page\r\n");
209 session_flush(s);
210 break;
211 }
212 if (c == '>')
213 page++;
214 else
215 page--;
216 find_message_ids = true;
217 show_list = true;
218 break;
219 case '#':
220 if (mn < 1 || mn > nmail_ids) {
221 session_printf(s, "Invalid message\r\n");
222 session_flush(s);
223 break;
224 }
225 ret = mail_read(s, mail_ids[mn - 1], mn);
226 switch (ret) {
227 case MAIL_READ_RETURN_DONE:
228 break;
229 case MAIL_READ_RETURN_LIST:
230 show_list = true;
231 break;
232 case MAIL_READ_RETURN_FIND:
233 find_message_ids = true;
234 show_list = true;
235 break;
236 default:
237 c = ret;
238 goto handle_opt;
239 }
240 break;
241 case '?':
242 show_help = true;
243 break;
244 default:
245 done = true;
246 break;
247 }
248 }
249
250 if (mail_ids != NULL)
251 xfree(&mail_ids);
252 }
253
254 void
255 mail_compose(struct session *s, char *initial_to, char *initial_subject,
256 char *initial_body, char *ftn_reply_msgid)
257 {
258 struct user *to_user = NULL;
259 struct mail_message msg = { 0 };
260 struct fidopkt_address ftn_our_address, ftn_to;
261 struct fidopkt_message ftnmsg = { 0 };
262 char *to_username = NULL, *tmp, *at;
263 char c;
264
265 if (!s->user) {
266 session_printf(s, "Mail is not available to guests.\r\n"
267 "Please create an account first.\r\n");
268 session_flush(s);
269 return;
270 }
271
272 fidopkt_parse_address(db->config.ftn_node_addr, &ftn_our_address);
273
274 if (initial_to) {
275 to_username = xstrdup(initial_to);
276 if (to_username == NULL)
277 goto mail_compose_done;
278 }
279 if (initial_subject) {
280 msg.subject = xstrdup(initial_subject);
281 if (msg.subject == NULL)
282 goto mail_compose_done;
283 }
284 if (initial_body) {
285 msg.body = xstrdup(initial_body);
286 if (msg.body == NULL)
287 goto mail_compose_done;
288 }
289
290 session_printf(s, "{{B}}Compose New Mail{{/B}}\r\n");
291 session_printf(s, "{{B}}From: {{/B}} %s\r\n", s->user->username);
292 session_flush(s);
293
294 mail_compose_start:
295 for (;;) {
296 session_printf(s, "{{B}}To: {{/B}} ");
297 session_flush(s);
298
299 tmp = session_field_input(s, 50, 50, to_username, false, 0);
300 if (to_username != NULL)
301 xfree(&to_username);
302 to_username = tmp;
303 if (to_username == NULL || to_username[0] == '\0') {
304 session_output(s, "\r\n", 2);
305 session_flush(s);
306 goto mail_compose_done;
307 }
308
309 if ((at = strstr(to_username, "@")) != NULL) {
310 if (ftn_our_address.zone == 0) {
311 session_printf(s, "\r\n{{B}}Error:{{/B}}{{#}} NetMail "
312 "is not supported at this time.\r\n");
313 session_flush(s);
314 xfree(&to_username);
315 continue;
316 }
317
318 if (!fidopkt_parse_address(at + 1, &ftn_to)) {
319 session_printf(s, "\r\n{{B}}Error:{{/B}}{{#}} Invalid "
320 "%s destination \"%s\" (^C to cancel)\r\n",
321 db->config.ftn_network, at + 1);
322 session_flush(s);
323 xfree(&to_username);
324 continue;
325 }
326 memcpy(&msg.ftn_dest, &ftn_to, sizeof(msg.ftn_dest));
327 strlcpy(msg.ftn_to, to_username, sizeof(msg.ftn_to));
328 if ((at = strstr(msg.ftn_to, "@")) != NULL)
329 at[0] = '\0';
330 strlcpy(msg.ftn_from, s->user->username,
331 sizeof(msg.ftn_from));
332 session_printf(s, " (%s)\r\n", db->config.ftn_network);
333 session_flush(s);
334 } else {
335 session_output(s, "\r\n", 2);
336 session_flush(s);
337 to_user = user_find_by_username(to_username);
338 if (to_user == NULL) {
339 session_printf(s, "{{B}}Error:{{/B}}{{#}} No such user "
340 "\"%s\" (^C to cancel)\r\n", to_username);
341 session_flush(s);
342 xfree(&to_username);
343 continue;
344 }
345 msg.recipient_user_id = to_user->id;
346 xfree(&to_user);
347 }
348 break;
349 }
350
351 for (;;) {
352 session_printf(s, "{{B}}Subject:{{/B}} ");
353 session_flush(s);
354
355 tmp = session_field_input(s, 60, 60, msg.subject, false, 0);
356 if (msg.subject != NULL)
357 xfree(&msg.subject);
358 msg.subject = tmp;
359 session_output(s, "\r\n", 2);
360 session_flush(s);
361
362 if (msg.subject == NULL)
363 goto mail_compose_done;
364
365 rtrim(msg.subject, "\r\n\t ");
366
367 if (msg.subject[0] == '\0') {
368 session_printf(s, "{{B}}Error:{{/B}} Subject cannot "
369 "be blank (^C to cancel)\r\n");
370 session_flush(s);
371 xfree(&msg.subject);
372 continue;
373 }
374 msg.subject_size = strlen(msg.subject) + 1;
375 break;
376 }
377
378 for (;;) {
379 session_printf(s,
380 "{{B}}Message (^D when finished):{{/B}}\r\n");
381 session_flush(s);
382
383 tmp = session_field_input(s, 2048, s->terminal_columns - 1,
384 msg.body, true, 0);
385 if (msg.body != NULL)
386 xfree(&msg.body);
387 msg.body = tmp;
388 session_output(s, "\r\n", 2);
389 session_flush(s);
390
391 if (msg.body == NULL)
392 goto mail_compose_done;
393
394 rtrim(msg.body, "\r\n\t ");
395
396 if (msg.body[0] == '\0') {
397 session_printf(s, "{{B}}Error:{{/B}} Message cannot "
398 "be blank (^C to cancel)\r\n");
399 session_flush(s);
400 xfree(&msg.body);
401 msg.body = NULL;
402 continue;
403 }
404 msg.body_size = strlen(msg.body) + 1;
405 break;
406 }
407
408 for (;;) {
409 session_printf(s, "\r\n{{B}}(S){{/B}}end ");
410 if (msg.ftn_to[0])
411 session_printf(s, "%s ", db->config.ftn_network);
412 session_printf(s, "message, "
413 "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? ");
414 session_flush(s);
415
416 c = session_input_char(s);
417 if (c == 0 && s->obuflen > 0) {
418 s->node_funcs->output(s);
419 uthread_yield();
420 if (s->ending)
421 goto mail_compose_done;
422 continue;
423 }
424
425 session_printf(s, "%c\r\n", c);
426 session_flush(s);
427
428 switch (c) {
429 case 's':
430 case 'S':
431 case 'y':
432 case '\n':
433 case '\r':
434 /* send */
435 if (msg.ftn_to[0])
436 session_printf(s, "Queueing %s NetMail for delivery...",
437 db->config.ftn_network);
438 else
439 session_printf(s, "Sending mail...");
440 session_flush(s);
441
442 msg.time = Time;
443 msg.sender_user_id = s->user->id;
444 msg.id = bile_next_id(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE);
445
446 if (msg.ftn_to[0]) {
447 if (ftn_reply_msgid != NULL && ftn_reply_msgid[0])
448 strlcpy(msg.ftn_reply, ftn_reply_msgid,
449 sizeof(msg.ftn_reply));
450
451 msg.ftn_msgid.id = 0x10FF0000 |
452 ((unsigned long)ftn_our_address.node << 24) | msg.id;
453 msg.ftn_msgid.zone = ftn_our_address.zone;
454 msg.ftn_msgid.net = ftn_our_address.net;
455 msg.ftn_msgid.node = ftn_our_address.node;
456 msg.ftn_msgid.point = ftn_our_address.point;
457 }
458
459 if (mail_save(&msg) != 0) {
460 session_printf(s, "failed!\r\n");
461 session_flush(s);
462 break;
463 }
464
465 if (msg.ftn_to[0]) {
466 ftnmsg.time = msg.time;
467 memcpy(&ftnmsg.header.orig, &ftn_our_address,
468 sizeof(ftnmsg.header.orig));
469 memcpy(&ftnmsg.header.dest, &msg.ftn_dest,
470 sizeof(ftnmsg.header.dest));
471 strlcpy(ftnmsg.to, msg.ftn_to, sizeof(ftnmsg.to));
472 strlcpy(ftnmsg.from, msg.ftn_from, sizeof(ftnmsg.from));
473 strlcpy(ftnmsg.subject, msg.subject,
474 sizeof(ftnmsg.subject));
475 ftnmsg.body = msg.body;
476 ftnmsg.body_len = msg.body_size - 1;
477 if (ftn_reply_msgid != NULL && ftn_reply_msgid[0])
478 strlcpy(ftnmsg.reply, ftn_reply_msgid,
479 sizeof(ftnmsg.reply));
480 ftnmsg.msgid = msg.ftn_msgid;
481 ftnmsg.attr = FIDOPKT_MSG_ATTR_PRIVATE;
482 if (!binkp_scan_message(&ftnmsg)) {
483 session_printf(s, "failed!\r\n");
484 session_flush(s);
485 break;
486 }
487 session_printf(s, " done!\r\n");
488 } else
489 session_printf(s, " sent!\r\n");
490 session_flush(s);
491
492 goto mail_compose_done;
493 break;
494 case 'e':
495 case 'E':
496 goto mail_compose_start;
497 case 'c':
498 case 'C':
499 case CONTROL_C:
500 goto mail_compose_done;
501 }
502 }
503
504 mail_compose_done:
505 if (to_username != NULL)
506 xfree(&to_username);
507
508 mail_free_message_strings(&msg);
509 }
510
511 void
512 mail_list(struct session *s, size_t nmail_ids, unsigned long *mail_ids,
513 size_t page, size_t pages)
514 {
515 char time[10], from[40];
516 size_t n, size;
517 struct mail_message msg;
518 struct username_cache *user;
519 char *data;
520
521 session_printf(s, "{{B}}Mail (Page %ld of %ld){{/B}}\r\n",
522 page, pages);
523 session_printf(s, "%s# Flg Date From Subject%s\r\n",
524 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
525 session_flush(s);
526
527 for (n = 0; n < nmail_ids; n++) {
528 size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE,
529 mail_ids[n], &data);
530 if (size == 0)
531 break;
532 bile_unmarshall_object(db->mail_bile, mail_object_fields,
533 nitems(mail_object_fields), data, size, &msg, sizeof(msg), true);
534 xfree(&data);
535
536 if (msg.ftn_from[0] && msg.recipient_user_id) {
537 size = snprintf(from, sizeof(from), "%s@%u:%u/%u",
538 msg.ftn_from, msg.ftn_msgid.zone,
539 msg.ftn_msgid.net, msg.ftn_msgid.node);
540 if (msg.ftn_msgid.point != 0)
541 snprintf(from + size, sizeof(from) - size,
542 ".%u", msg.ftn_msgid.point);
543 } else if (msg.sender_user_id == 0) {
544 strlcpy(from, "(System)", sizeof(from));
545 } else {
546 user = user_username(msg.sender_user_id);
547 strlcpy(from, user ? user->username : "(unknown)",
548 sizeof(from));
549 }
550
551 strftime(time, sizeof(time), "%b %d", localtime(&msg.time));
552
553 session_printf(s, "%s%ld %c %s %-15.15s {{#}}%.40s%s\r\n",
554 msg.read ? "" : ansi(s, ANSI_BOLD, ANSI_END),
555 n + 1,
556 msg.read ? ' ' : 'N',
557 time,
558 from,
559 msg.subject,
560 msg.read ? "" : ansi(s, ANSI_RESET, ANSI_END));
561
562 mail_free_message_strings(&msg);
563 }
564
565 session_flush(s);
566 }
567
568 short
569 mail_read(struct session *s, unsigned long id, short idx)
570 {
571 static const struct session_menu_option opts[] = {
572 { 'r', "Rr", "Reply to this message" },
573 { 'd', "Dd", "Delete this message" },
574 { 'u', "Uu", "Mark this message unread" },
575 { 'm', "MmNn", "Compose new mail message" },
576 { 'l', "Ll", "Return and list mail messages" },
577 { 'q', "QqXx", "Return to message list" },
578 { '?', "?", "Show this help menu" },
579 };
580 static const char prompt_help[] =
581 "R:Reply D:Delete U:Unread M:New Msg L:List Q:Return ?:Help";
582 char time[32];
583 char prompt[24];
584 char title[50];
585 char from[50], to[50];
586 size_t size;
587 struct mail_message msg;
588 struct username_cache *user;
589 short ret = MAIL_READ_RETURN_DONE;
590 char *data, *reply_subject;
591 bool done = false, show_help = false;
592 char c;
593
594 size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, id,
595 &data);
596 if (size == 0) {
597 session_printf(s, "{{B}}Error:{{/B}} Can't find message\r\n");
598 session_flush(s);
599 return MAIL_READ_RETURN_DONE;
600 }
601
602 bile_unmarshall_object(db->mail_bile, mail_object_fields,
603 nitems(mail_object_fields), data, size, &msg, sizeof(msg), true);
604 xfree(&data);
605
606 if (msg.ftn_from[0]) {
607 size = snprintf(from, sizeof(from), "%s@%u:%u/%u",
608 msg.ftn_from, msg.ftn_msgid.zone, msg.ftn_msgid.net,
609 msg.ftn_msgid.node);
610 if (msg.ftn_msgid.point)
611 snprintf(from + size, sizeof(from) - size, ".%u",
612 msg.ftn_msgid.point);
613 } else if (msg.sender_user_id == 0) {
614 strlcpy(from, "(System)", sizeof(from));
615 } else {
616 user = user_username(msg.sender_user_id);
617 if (user)
618 strlcpy(from, user->username, sizeof(from));
619 else
620 strlcpy(from, "(Unknown)", sizeof(from));
621 }
622
623 if (msg.ftn_to[0]) {
624 size = snprintf(to, sizeof(to), "%s@%u:%u/%u",
625 msg.ftn_to, msg.ftn_dest.zone, msg.ftn_dest.net,
626 msg.ftn_dest.node);
627 if (msg.ftn_dest.point)
628 snprintf(to + size, sizeof(to) - size, ".%u",
629 msg.ftn_dest.point);
630 } else {
631 user = user_username(msg.recipient_user_id);
632 if (user)
633 strlcpy(to, user->username, sizeof(to));
634 else
635 strlcpy(to, "(Unknown)", sizeof(to));
636 }
637
638 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
639 localtime(&msg.time));
640
641 session_printf(s, "{{B}}From:{{/B}} %s\r\n", from);
642 session_printf(s, "{{B}}To:{{/B}} %s\r\n", to);
643 session_printf(s, "{{B}}Date:{{/B}} %s %s\r\n", time,
644 db->config.timezone);
645 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s\r\n", msg.subject);
646 session_printf(s, "\r\n");
647 session_flush(s);
648 session_paginate(s, msg.body, msg.body_size, 5);
649
650 if (!msg.read) {
651 msg.read = Time;
652 if (mail_save(&msg) != 0)
653 session_printf(s, "Failed marking message read!\r\n");
654 }
655
656 snprintf(prompt, sizeof(prompt), "Mail:Message %d", idx);
657 snprintf(title, sizeof(title), "Mail: Message %d", idx);
658
659 while (!done && !s->ending) {
660 c = session_menu(s, title, prompt, (char *)prompt_help, opts,
661 nitems(opts), show_help, NULL, NULL);
662 show_help = false;
663
664 switch (c) {
665 case 'r':
666 if (msg.sender_user_id == 0) {
667 session_printf(s, "System messages cannot be replied "
668 "to.\r\n");
669 break;
670 }
671
672 reply_subject = xmalloc(strlen(msg.subject) + 5);
673 if (reply_subject == NULL)
674 break;
675 if (strncmp(msg.subject, "Re:", 3) == 0)
676 strlcpy(reply_subject, msg.subject,
677 strlen(msg.subject) + 1);
678 else
679 sprintf(reply_subject, "Re: %s", msg.subject);
680
681 mail_compose(s, from, reply_subject, NULL,
682 msg.ftn_msgid_orig[0] ? msg.ftn_msgid_orig : NULL);
683 xfree(&reply_subject);
684 break;
685 case 'd':
686 if (bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, msg.id,
687 BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE) == 0)
688 session_printf(s, "Deleted message {{B}}%d{{/B}}\r\n", idx);
689 else
690 session_printf(s, "Failed deleting message "
691 "{{B}}%d{{/B}}! (%d)\r\n", idx,
692 bile_error(db->mail_bile));
693 ret = MAIL_READ_RETURN_FIND;
694 done = true;
695 break;
696 case 'u':
697 msg.read = 0;
698 if (mail_save(&msg) == 0)
699 session_printf(s, "Marked message {{B}}%d{{/B}} unread\r\n",
700 idx);
701 else
702 session_printf(s, "Failed updating message "
703 "{{B}}%d{{/B}}! (%d)\r\n", idx,
704 bile_error(db->mail_bile));
705 ret = MAIL_READ_RETURN_DONE;
706 done = true;
707 break;
708 case 'm':
709 mail_compose(s, NULL, NULL, NULL, NULL);
710 break;
711 case 'l':
712 done = true;
713 ret = MAIL_READ_RETURN_LIST;
714 break;
715 case 'q':
716 done = true;
717 ret = MAIL_READ_RETURN_DONE;
718 break;
719 case '?':
720 show_help = true;
721 break;
722 }
723 }
724
725 mail_free_message_strings(&msg);
726
727 return ret;
728 }
729
730 short
731 mail_save(struct mail_message *msg)
732 {
733 size_t size;
734 char *data;
735 short ret;
736
737 if (!msg->id)
738 msg->id = bile_next_id(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE);
739 if (!msg->id)
740 return -1;
741
742 ret = bile_marshall_object(db->mail_bile, mail_object_fields,
743 nitems(mail_object_fields), msg, &data, &size);
744 if (ret != 0 || size == 0) {
745 warn("mail_save: failed to marshall object");
746 return -1;
747 }
748
749 if (bile_write(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, msg->id, data,
750 size) != size) {
751 warn("mail_save: bile_write failed! %d", bile_error(db->mail_bile));
752 return bile_error(db->mail_bile);
753 }
754
755 return 0;
756 }
757
758 /*
759 * return count of all ids for the user (for pagination), set nmail_ids to
760 * the count of the ids set in mail_ids based on offset/limit/only_unread
761 *
762 * nret_mail_ids or ret_mail_ids may be NULL to avoid building an array
763 */
764 size_t
765 mail_find_ids_for_user(struct user *user, size_t *nret_mail_ids,
766 unsigned long **ret_mail_ids, size_t offset, size_t limit,
767 bool only_unread)
768 {
769 struct mail_message msg;
770 size_t n, nall_mail_ids, nmail_ids, mail_ids_size, nlimit_mail_ids,
771 id, size;
772 bool failed = false;
773 unsigned long *mail_ids = NULL, *all_mail_ids;
774 short i, j;
775
776 if (ret_mail_ids != NULL)
777 *ret_mail_ids = NULL;
778 if (nret_mail_ids != NULL)
779 *nret_mail_ids = 0;
780
781 nall_mail_ids = bile_ids_by_type(db->mail_bile,
782 MAIL_SPOOL_MESSAGE_RTYPE, &all_mail_ids);
783
784 /* narrow all_mail_ids down to mail_ids for this user */
785 mail_ids_size = 0;
786 nmail_ids = 0;
787 for (n = 0; n < nall_mail_ids; n++) {
788 /* read only as far as we need to */
789 size = offsetof(struct mail_message, read) +
790 member_size(struct mail_message, read);
791 if (bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE,
792 all_mail_ids[n], (char *)&msg, size) != size) {
793 failed = true;
794 goto done;
795 }
796
797 if (msg.recipient_user_id != user->id)
798 continue;
799 if (only_unread && msg.read)
800 continue;
801
802 if (ret_mail_ids != NULL) {
803 if (!grow_to_fit(&mail_ids, &mail_ids_size,
804 (nmail_ids + 1) * sizeof(long), sizeof(long),
805 sizeof(long) * 16)) {
806 failed = true;
807 goto done;
808 }
809 mail_ids[nmail_ids] = all_mail_ids[n];
810 }
811
812 nmail_ids++;
813 }
814
815 if (all_mail_ids != NULL)
816 xfree(&all_mail_ids);
817
818 if (ret_mail_ids != NULL) {
819 /* sort by message id descending for consistent ordering */
820 for (i = 1; i < nmail_ids; i++) {
821 for (j = i; j > 0; j--) {
822 if (mail_ids[j] < mail_ids[j - 1])
823 break;
824 id = mail_ids[j];
825 mail_ids[j] = mail_ids[j - 1];
826 mail_ids[j - 1] = id;
827 }
828 }
829 }
830
831 nlimit_mail_ids = nmail_ids;
832
833 if (offset) {
834 if (offset >= nlimit_mail_ids) {
835 if (mail_ids != NULL)
836 xfree(&mail_ids);
837 nlimit_mail_ids = 0;
838 } else {
839 if (mail_ids != NULL) {
840 for (j = offset, i = 0; j < nmail_ids; j++, i++)
841 mail_ids[i] = mail_ids[j];
842 }
843 nlimit_mail_ids -= offset;
844 }
845 }
846
847 if (limit && nlimit_mail_ids > limit)
848 nlimit_mail_ids = limit;
849
850 if (ret_mail_ids != NULL)
851 *ret_mail_ids = mail_ids;
852 if (nret_mail_ids != NULL)
853 *nret_mail_ids = nlimit_mail_ids;
854
855 done:
856 if (failed)
857 return 0;
858 return nmail_ids;
859 }
860
861 short
862 mail_toss_ftn_message(struct fidopkt_message *ftnmsg)
863 {
864 struct user *to = NULL;
865 struct fidopkt_address our_address;
866 size_t n, nmail_ids, size;
867 unsigned long *mail_ids = NULL, sysop_id;
868 struct mail_message msg;
869 short ret = 1;
870 char *data;
871
872 if (db->config.ftn_node_addr[0] == 0) {
873 logger_printf("[mail] Local FTN node address must be "
874 "configured in settings");
875 return -1;
876 }
877
878 if (!fidopkt_parse_address(db->config.ftn_node_addr,
879 &our_address)) {
880 logger_printf("[mail] Failed parsing local FTN node address "
881 "\"%s\", fix in settings", db->config.ftn_node_addr);
882 return -1;
883 }
884
885 if (memcmp(&ftnmsg->header.dest, &our_address,
886 sizeof(struct fidopkt_address)) != 0) {
887 logger_printf("[mail] NetMail message is destined for "
888 "%u:%u/%u.%u, not us (%u:%u/%u.%u)",
889 ftnmsg->header.dest.zone, ftnmsg->header.dest.net,
890 ftnmsg->header.dest.node, ftnmsg->header.dest.point,
891 our_address.zone, our_address.net,
892 our_address.node, our_address.point);
893 ret = 0;
894 goto done;
895 }
896
897 to = user_find_by_username(ftnmsg->to);
898 if (to == NULL) {
899 sysop_id = user_first_sysop_id();
900 if (!sysop_id || !(to = user_find(sysop_id))) {
901 logger_printf("[mail] Can't find sysop username?");
902 ret = 0;
903 goto done;
904 }
905
906 logger_printf("[mail] No local user \"%s\", bouncing to sysop %s",
907 ftnmsg->to, to->username);
908 }
909
910 mail_find_ids_for_user(to, &nmail_ids, &mail_ids, 0, 0, false);
911 for (n = 0; n < nmail_ids; n++) {
912 size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE,
913 mail_ids[n], &data);
914 if (size == 0)
915 break;
916 bile_unmarshall_object(db->mail_bile, mail_object_fields,
917 nitems(mail_object_fields), data, size, &msg, sizeof(msg), false);
918 xfree(&data);
919
920 if (memcmp(&msg.ftn_msgid, &ftnmsg->msgid,
921 sizeof(struct fidopkt_msgid)) == 0) {
922 logger_printf("[mail] Already have %s NetMail message %s "
923 "(%ld), skipping", db->config.ftn_network,
924 ftnmsg->msgid_orig, msg.id);
925 ret = 0;
926 break;
927 }
928 }
929 if (mail_ids != NULL)
930 xfree(&mail_ids);
931
932 if (ret < 1) {
933 xfree(&to);
934 return ret;
935 }
936
937 memset(&msg, 0, sizeof(msg));
938 msg.recipient_user_id = to->id;
939 msg.time = ftnmsg->time;
940 msg.subject = xstrdup(ftnmsg->subject);
941 msg.subject_size = strlen(ftnmsg->subject) + 1;
942 msg.body = ftnmsg->body;
943 msg.body_size = ftnmsg->body_len + 1;
944
945 msg.ftn_msgid = ftnmsg->msgid;
946 msg.ftn_orig = ftnmsg->header.orig;
947 msg.ftn_dest = ftnmsg->header.dest;
948
949 strlcpy(msg.ftn_from, ftnmsg->from, sizeof(msg.ftn_from));
950 strlcpy(msg.ftn_msgid_orig, ftnmsg->msgid_orig,
951 sizeof(msg.ftn_msgid_orig));
952 strlcpy(msg.ftn_reply, ftnmsg->reply, sizeof(msg.ftn_reply));
953
954 if (mail_save(&msg) != 0) {
955 logger_printf("[binkp] Failed saving %s NetMail message from "
956 "%u:%u/%u.%u to local user %s", db->config.ftn_network,
957 ftnmsg->header.orig.zone, ftnmsg->header.orig.net,
958 ftnmsg->header.orig.node, ftnmsg->header.orig.point,
959 to->username);
960 ret = -1;
961 goto done;
962 }
963
964 logger_printf("[binkp] Tossed %s NetMail from %u:%u/%u.%u to local "
965 "user %s", db->config.ftn_network,
966 ftnmsg->header.orig.zone, ftnmsg->header.orig.net,
967 ftnmsg->header.orig.node, ftnmsg->header.orig.point, to->username);
968
969 done:
970 if (to != NULL)
971 xfree(&to);
972 return ret;
973 }
974
975 void
976 mail_to_sysop(char *subject, char *body)
977 {
978 struct mail_message msg;
979 struct username_cache *ucache;
980 unsigned long sysop_id;
981
982 sysop_id = user_first_sysop_id();
983 if (!sysop_id) {
984 logger_printf("[mail] Tried mailing sysop, none found");
985 return;
986 }
987
988 memset(&msg, 0, sizeof(msg));
989 msg.recipient_user_id = sysop_id;
990 msg.time = Time;
991 msg.subject = subject;
992 msg.subject_size = strlen(subject) + 1;
993 msg.body = body;
994 msg.body_size = strlen(body) + 1;
995
996 if (mail_save(&msg) != 0) {
997 logger_printf("[mail] Failed saving new system mail to sysop");
998 return;
999 }
1000
1001 ucache = user_username(sysop_id);
1002
1003 logger_printf("[mail] Sent system mail to %s", ucache ?
1004 ucache->username : "(no username)");
1005 }
1006
1007 void
1008 mail_prune(short days)
1009 {
1010 struct mail_message msg;
1011 size_t n, nids, size, deleted, delta;
1012 unsigned long secs, *ids = NULL;
1013
1014 if (days < 1)
1015 return;
1016
1017 secs = (60UL * 60UL * 24UL * (unsigned long)days);
1018
1019 logger_printf("[db] Pruning mail older than %d days", days);
1020
1021 nids = bile_ids_by_type(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE,
1022 &ids);
1023 if (nids == 0)
1024 return;
1025
1026 size = offsetof(struct mail_message, time) +
1027 member_size(struct mail_message, time);
1028 deleted = 0;
1029 for (n = 0; n < nids; n++) {
1030 if (bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, ids[n],
1031 &msg, size) != size)
1032 break;
1033
1034 delta = Time - msg.time;
1035 if (delta > secs) {
1036 deleted++;
1037 bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, ids[n], 0);
1038 uthread_yield();
1039 }
1040 }
1041 xfree(&ids);
1042
1043 if (deleted) {
1044 logger_printf("[db] Deleted %ld of %ld mail messages", deleted,
1045 nids);
1046 bile_write_map(db->mail_bile);
1047 }
1048 }