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 | } |