AmendHub

Download

jcs

/

subtext

/

mail.c

 

(View History)

jcs   session: Turn word wrap into paginator, use in boards+mail Latest amendment: 467 on 2023-04-07

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(ftn_reply_msgid));
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;
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 snprintf(from, sizeof(from), "%s@%u:%u/%u.%u",
538 msg.ftn_from, msg.ftn_msgid.zone,
539 msg.ftn_msgid.net, msg.ftn_msgid.node,
540 msg.ftn_msgid.point);
541 } else {
542 user = user_username(msg.sender_user_id);
543 strlcpy(from, user ? user->username : "(unknown)",
544 sizeof(from));
545 }
546
547 strftime(time, sizeof(time), "%b %d", localtime(&msg.time));
548
549 session_printf(s, "%s%ld %c %s %-15.15s {{#}}%.40s%s\r\n",
550 msg.read ? "" : ansi(s, ANSI_BOLD, ANSI_END),
551 n + 1,
552 msg.read ? ' ' : 'N',
553 time,
554 from,
555 msg.subject,
556 msg.read ? "" : ansi(s, ANSI_RESET, ANSI_END));
557
558 mail_free_message_strings(&msg);
559 }
560
561 session_flush(s);
562 }
563
564 short
565 mail_read(struct session *s, unsigned long id, short idx)
566 {
567 static const struct session_menu_option opts[] = {
568 { 'r', "Rr", "Reply to this message" },
569 { 'd', "Dd", "Delete this message" },
570 { 'u', "Uu", "Mark this message unread" },
571 { 'm', "MmNn", "Compose new mail message" },
572 { 'l', "Ll", "Return and list mail messages" },
573 { 'q', "QqXx", "Return to message list" },
574 { '?', "?", "Show this help menu" },
575 };
576 static const char prompt_help[] =
577 "R:Reply D:Delete U:Unread M:New Msg L:List Q:Return ?:Help";
578 char time[32];
579 char prompt[24];
580 char title[50];
581 char from[50], to[50];
582 size_t size;
583 struct mail_message msg;
584 struct username_cache *user;
585 short ret = MAIL_READ_RETURN_DONE;
586 char *data, *reply_subject;
587 bool done = false, show_help = false;
588 char c;
589
590 size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, id,
591 &data);
592 if (size == 0) {
593 session_printf(s, "{{B}}Error:{{/B}} Can't find message\r\n");
594 session_flush(s);
595 return MAIL_READ_RETURN_DONE;
596 }
597
598 bile_unmarshall_object(db->mail_bile, mail_object_fields,
599 nitems(mail_object_fields), data, size, &msg, sizeof(msg), true);
600 xfree(&data);
601
602 if (msg.ftn_from[0]) {
603 size = snprintf(from, sizeof(from), "%s@%u:%u/%u",
604 msg.ftn_from, msg.ftn_msgid.zone, msg.ftn_msgid.net,
605 msg.ftn_msgid.node);
606 if (msg.ftn_msgid.point)
607 snprintf(from + size, sizeof(from) - size, ".%u",
608 msg.ftn_msgid.point);
609 } else {
610 user = user_username(msg.sender_user_id);
611 if (user)
612 strlcpy(from, user->username, sizeof(from));
613 else
614 strlcpy(from, "(Unknown)", sizeof(from));
615 }
616
617 if (msg.ftn_to[0]) {
618 snprintf(to, sizeof(to), "%s@%u:%u/%u",
619 msg.ftn_to, msg.ftn_dest.zone, msg.ftn_dest.net,
620 msg.ftn_dest.node);
621 if (msg.ftn_dest.point)
622 snprintf(to + size, sizeof(to) - size, ".%u",
623 msg.ftn_dest.point);
624 } else {
625 user = user_username(msg.recipient_user_id);
626 if (user)
627 strlcpy(to, user->username, sizeof(to));
628 else
629 strlcpy(to, "(Unknown)", sizeof(to));
630 }
631
632 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
633 localtime(&msg.time));
634
635 session_printf(s, "{{B}}From:{{/B}} %s\r\n", from);
636 session_printf(s, "{{B}}To:{{/B}} %s\r\n", to);
637 session_printf(s, "{{B}}Date:{{/B}} %s %s\r\n", time,
638 db->config.timezone);
639 session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s\r\n", msg.subject);
640 session_printf(s, "\r\n");
641 session_flush(s);
642 session_paginate(s, msg.body, msg.body_size, 5);
643
644 if (!msg.read) {
645 msg.read = Time;
646 if (mail_save(&msg) != 0)
647 session_printf(s, "Failed marking message read!\r\n");
648 }
649
650 snprintf(prompt, sizeof(prompt), "Mail:Message %d", idx);
651 snprintf(title, sizeof(title), "Mail: Message %d", idx);
652
653 while (!done && !s->ending) {
654 c = session_menu(s, title, prompt, (char *)prompt_help, opts,
655 nitems(opts), show_help, NULL, NULL);
656 show_help = false;
657
658 switch (c) {
659 case 'r':
660 reply_subject = xmalloc(strlen(msg.subject) + 5);
661 if (reply_subject == NULL)
662 break;
663 if (strncmp(msg.subject, "Re:", 3) == 0)
664 strlcpy(reply_subject, msg.subject,
665 strlen(msg.subject) + 1);
666 else
667 sprintf(reply_subject, "Re: %s", msg.subject);
668
669 mail_compose(s, from, reply_subject, NULL,
670 msg.ftn_msgid_orig[0] ? msg.ftn_msgid_orig : NULL);
671 xfree(&reply_subject);
672 break;
673 case 'd':
674 if (bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, msg.id,
675 BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE) == 0)
676 session_printf(s, "Deleted message {{B}}%d{{/B}}\r\n", idx);
677 else
678 session_printf(s, "Failed deleting message "
679 "{{B}}%d{{/B}}! (%d)\r\n", idx,
680 bile_error(db->mail_bile));
681 ret = MAIL_READ_RETURN_FIND;
682 done = true;
683 break;
684 case 'u':
685 msg.read = 0;
686 if (mail_save(&msg) == 0)
687 session_printf(s, "Marked message {{B}}%d{{/B}} unread\r\n",
688 idx);
689 else
690 session_printf(s, "Failed updating message "
691 "{{B}}%d{{/B}}! (%d)\r\n", idx,
692 bile_error(db->mail_bile));
693 ret = MAIL_READ_RETURN_DONE;
694 done = true;
695 break;
696 case 'm':
697 mail_compose(s, NULL, NULL, NULL, NULL);
698 break;
699 case 'l':
700 done = true;
701 ret = MAIL_READ_RETURN_LIST;
702 break;
703 case 'q':
704 done = true;
705 ret = MAIL_READ_RETURN_DONE;
706 break;
707 case '?':
708 show_help = true;
709 break;
710 }
711 }
712
713 mail_free_message_strings(&msg);
714
715 return ret;
716 }
717
718 short
719 mail_save(struct mail_message *msg)
720 {
721 size_t size;
722 char *data;
723 short ret;
724
725 if (!msg->id)
726 msg->id = bile_next_id(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE);
727 if (!msg->id)
728 return -1;
729
730 ret = bile_marshall_object(db->mail_bile, mail_object_fields,
731 nitems(mail_object_fields), msg, &data, &size);
732 if (ret != 0 || size == 0) {
733 warn("mail_save: failed to marshall object");
734 return -1;
735 }
736
737 if (bile_write(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, msg->id, data,
738 size) != size) {
739 warn("mail_save: bile_write failed! %d", bile_error(db->mail_bile));
740 return bile_error(db->mail_bile);
741 }
742
743 return 0;
744 }
745
746 /*
747 * return count of all ids for the user (for pagination), set nmail_ids to
748 * the count of the ids set in mail_ids based on offset/limit/only_unread
749 *
750 * nret_mail_ids or ret_mail_ids may be NULL to avoid building an array
751 */
752 size_t
753 mail_find_ids_for_user(struct user *user, size_t *nret_mail_ids,
754 unsigned long **ret_mail_ids, size_t offset, size_t limit,
755 bool only_unread)
756 {
757 struct mail_message msg;
758 size_t n, nall_mail_ids, nmail_ids, mail_ids_size, nlimit_mail_ids,
759 id, size;
760 bool failed = false;
761 unsigned long *mail_ids = NULL, *all_mail_ids;
762 short i, j;
763
764 if (ret_mail_ids != NULL)
765 *ret_mail_ids = NULL;
766 if (nret_mail_ids != NULL)
767 *nret_mail_ids = 0;
768
769 nall_mail_ids = bile_ids_by_type(db->mail_bile,
770 MAIL_SPOOL_MESSAGE_RTYPE, &all_mail_ids);
771
772 /* narrow all_mail_ids down to mail_ids for this user */
773 mail_ids_size = 0;
774 nmail_ids = 0;
775 for (n = 0; n < nall_mail_ids; n++) {
776 /* read only as far as we need to */
777 size = offsetof(struct mail_message, read) +
778 member_size(struct mail_message, read);
779 if (bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE,
780 all_mail_ids[n], (char *)&msg, size) != size) {
781 failed = true;
782 goto done;
783 }
784
785 if (msg.recipient_user_id != user->id)
786 continue;
787 if (only_unread && msg.read)
788 continue;
789
790 if (ret_mail_ids != NULL) {
791 if (!grow_to_fit(&mail_ids, &mail_ids_size,
792 (nmail_ids + 1) * sizeof(long), sizeof(long),
793 sizeof(long) * 16)) {
794 failed = true;
795 goto done;
796 }
797 mail_ids[nmail_ids] = all_mail_ids[n];
798 }
799
800 nmail_ids++;
801 }
802
803 if (all_mail_ids != NULL)
804 xfree(&all_mail_ids);
805
806 if (ret_mail_ids != NULL) {
807 /* sort by message id descending for consistent ordering */
808 for (i = 0; i < nmail_ids; i++) {
809 for (j = 0; j < nmail_ids - i - 1; j++) {
810 if (mail_ids[j] < mail_ids[j + 1]) {
811 id = mail_ids[j];
812 mail_ids[j] = mail_ids[j + 1];
813 mail_ids[j + 1] = id;
814 }
815 }
816 }
817 }
818
819 nlimit_mail_ids = nmail_ids;
820
821 if (offset) {
822 if (offset >= nlimit_mail_ids) {
823 if (mail_ids != NULL)
824 xfree(&mail_ids);
825 nlimit_mail_ids = 0;
826 } else {
827 if (mail_ids != NULL) {
828 for (j = offset, i = 0; j < nmail_ids; j++, i++)
829 mail_ids[i] = mail_ids[j];
830 }
831 nlimit_mail_ids -= offset;
832 }
833 }
834
835 if (limit && nlimit_mail_ids > limit)
836 nlimit_mail_ids = limit;
837
838 if (ret_mail_ids != NULL)
839 *ret_mail_ids = mail_ids;
840 if (nret_mail_ids != NULL)
841 *nret_mail_ids = nlimit_mail_ids;
842
843 done:
844 if (failed)
845 return 0;
846 return nmail_ids;
847 }
848
849 short
850 mail_toss_ftn_message(struct fidopkt_message *ftnmsg)
851 {
852 struct user *to = NULL;
853 struct fidopkt_address our_address;
854 size_t n, nmail_ids, size;
855 unsigned long *mail_ids = NULL;
856 struct mail_message msg;
857 short ret = 1;
858 char *data;
859
860 if (db->config.ftn_node_addr[0] == 0) {
861 logger_printf("[mail] Local FTN node address must be "
862 "configured in settings");
863 return -1;
864 }
865
866 if (!fidopkt_parse_address(db->config.ftn_node_addr,
867 &our_address)) {
868 logger_printf("[mail] Failed parsing local FTN node address "
869 "\"%s\", fix in settings", db->config.ftn_node_addr);
870 return -1;
871 }
872
873 if (memcmp(&ftnmsg->header.dest, &our_address,
874 sizeof(struct fidopkt_address)) != 0) {
875 logger_printf("[mail] NetMail message is destined for "
876 "%u:%u/%u.%u, not us (%u:%u/%u.%u)",
877 ftnmsg->header.dest.zone, ftnmsg->header.dest.net,
878 ftnmsg->header.dest.node, ftnmsg->header.dest.point,
879 our_address.zone, our_address.net,
880 our_address.node, our_address.point);
881 ret = 0;
882 goto done;
883 }
884
885 to = user_find_by_username(ftnmsg->to);
886 if (to == NULL) {
887 to = user_find_by_username(user_first_sysop_username());
888 if (to == NULL) {
889 logger_printf("[mail] Can't find sysop username?");
890 ret = 0;
891 goto done;
892 }
893 logger_printf("[mail] No local user \"%s\", bouncing to sysop %s",
894 ftnmsg->to, to->username);
895 }
896
897 mail_find_ids_for_user(to, &nmail_ids, &mail_ids, 0, 0, false);
898 for (n = 0; n < nmail_ids; n++) {
899 size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE,
900 mail_ids[n], &data);
901 if (size == 0)
902 break;
903 bile_unmarshall_object(db->mail_bile, mail_object_fields,
904 nitems(mail_object_fields), data, size, &msg, sizeof(msg), false);
905 xfree(&data);
906
907 if (memcmp(&msg.ftn_msgid, &ftnmsg->msgid,
908 sizeof(struct fidopkt_msgid)) == 0) {
909 logger_printf("[mail] Already have %s NetMail message %s "
910 "(%ld), skipping", db->config.ftn_network,
911 ftnmsg->msgid_orig, msg.id);
912 ret = 0;
913 break;
914 }
915 }
916 if (mail_ids != NULL)
917 xfree(&mail_ids);
918
919 if (ret < 1) {
920 xfree(&to);
921 return ret;
922 }
923
924 memset(&msg, 0, sizeof(msg));
925 msg.recipient_user_id = to->id;
926 msg.time = ftnmsg->time;
927 msg.subject = xstrdup(ftnmsg->subject);
928 msg.subject_size = strlen(ftnmsg->subject) + 1;
929 msg.body = ftnmsg->body;
930 msg.body_size = ftnmsg->body_len + 1;
931
932 msg.ftn_msgid = ftnmsg->msgid;
933 msg.ftn_orig = ftnmsg->header.orig;
934 msg.ftn_dest = ftnmsg->header.dest;
935
936 strlcpy(msg.ftn_from, ftnmsg->from, sizeof(msg.ftn_from));
937 strlcpy(msg.ftn_msgid_orig, ftnmsg->msgid_orig,
938 sizeof(msg.ftn_msgid_orig));
939 strlcpy(msg.ftn_reply, ftnmsg->reply, sizeof(msg.ftn_reply));
940
941 if (mail_save(&msg) != 0) {
942 logger_printf("[binkp] Failed saving %s NetMail message from "
943 "%u:%u/%u.%u to local user %s", db->config.ftn_network,
944 ftnmsg->header.orig.zone, ftnmsg->header.orig.net,
945 ftnmsg->header.orig.node, ftnmsg->header.orig.point,
946 to->username);
947 ret = -1;
948 goto done;
949 }
950
951 logger_printf("[binkp] Tossed %s NetMail from %u:%u/%u.%u to local "
952 "user %s", db->config.ftn_network,
953 ftnmsg->header.orig.zone, ftnmsg->header.orig.net,
954 ftnmsg->header.orig.node, ftnmsg->header.orig.point, to->username);
955
956 done:
957 if (to != NULL)
958 xfree(&to);
959 return ret;
960 }
961
962 void
963 mail_prune(short days)
964 {
965 struct mail_message msg;
966 size_t n, nids, size, deleted, delta;
967 unsigned long secs, *ids = NULL;
968
969 if (days < 1)
970 return;
971
972 secs = (60UL * 60UL * 24UL * (unsigned long)days);
973
974 logger_printf("[db] Pruning mail older than %d days", days);
975
976 nids = bile_ids_by_type(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE,
977 &ids);
978 if (nids == 0)
979 return;
980
981 size = offsetof(struct mail_message, time) +
982 member_size(struct mail_message, time);
983 deleted = 0;
984 for (n = 0; n < nids; n++) {
985 if (bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, ids[n],
986 &msg, size) != size)
987 break;
988
989 delta = Time - msg.time;
990 if (delta > secs) {
991 deleted++;
992 bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, ids[n], 0);
993 uthread_yield();
994 }
995 }
996 xfree(&ids);
997
998 if (deleted) {
999 logger_printf("[db] Deleted %ld of %ld mail messages", deleted,
1000 nids);
1001 bile_write_map(db->mail_bile);
1002 }
1003 }