/* * Copyright (c) 2022 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include "ansi.h" #include "binkp.h" #include "logger.h" #include "mail.h" #include "subtext.h" #include "session.h" #include "user.h" #include "uthread.h" #include "util.h" #define MSGS_PER_PAGE 20 #define MAIL_READ_RETURN_DONE -1 #define MAIL_READ_RETURN_LIST -2 #define MAIL_READ_RETURN_FIND -3 static const struct bile_object_field mail_object_fields[] = { { offsetof(struct mail_message, recipient_user_id), member_size(struct mail_message, recipient_user_id), -1 }, { offsetof(struct mail_message, id), member_size(struct mail_message, id), -1 }, { offsetof(struct mail_message, time), member_size(struct mail_message, time), -1 }, { offsetof(struct mail_message, read), member_size(struct mail_message, read), -1 }, { offsetof(struct mail_message, sender_user_id), member_size(struct mail_message, sender_user_id), -1 }, { offsetof(struct mail_message, subject_size), member_size(struct mail_message, subject_size), -1 }, { offsetof(struct mail_message, subject), -1, offsetof(struct mail_message, subject_size) }, { offsetof(struct mail_message, body_size), member_size(struct mail_message, body_size), -1 }, { offsetof(struct mail_message, body), -1, offsetof(struct mail_message, body_size) }, { offsetof(struct mail_message, parent_message_id), member_size(struct mail_message, parent_message_id), -1 }, { offsetof(struct mail_message, ftn_msgid), member_size(struct mail_message, ftn_msgid), -1 }, { offsetof(struct mail_message, ftn_orig), member_size(struct mail_message, ftn_orig), -1 }, { offsetof(struct mail_message, ftn_dest), member_size(struct mail_message, ftn_dest), -1 }, { offsetof(struct mail_message, ftn_from), member_size(struct mail_message, ftn_from), -1 }, { offsetof(struct mail_message, ftn_to), member_size(struct mail_message, ftn_to), -1 }, { offsetof(struct mail_message, ftn_msgid_orig), member_size(struct mail_message, ftn_msgid_orig), -1 }, { offsetof(struct mail_message, ftn_reply), member_size(struct mail_message, ftn_reply), -1 }, }; static const size_t nmail_object_fields = nitems(mail_object_fields); void mail_free_message_strings(struct mail_message *msg); short mail_read(struct session *s, unsigned long id, short idx); void mail_list(struct session *s, size_t nmsgs, unsigned long *mail_ids, size_t page, size_t pages); short mail_get_message_id(struct session *s, size_t nmsgs, char *prompt, short initial); void mail_free_message_strings(struct mail_message *msg) { if (msg->subject != NULL) { xfree(&msg->subject); msg->subject = NULL; } if (msg->body != NULL) { xfree(&msg->body); msg->body = NULL; } } short mail_get_message_id(struct session *s, size_t nmsgs, char *prompt, short initial) { char *tmp = NULL; char start_s[10] = { 0 }; short ret; if (initial > -1) snprintf(start_s, sizeof(start_s), "%d", initial); get_id: if (prompt) session_printf(s, "{{B}}%s:{{/B}} ", prompt); tmp = session_field_input(s, 5, 5, start_s, false, 0); session_output(s, "\r\n", 2); session_flush(s); if (tmp == NULL || s->ending) { ret = -1; goto get_id_done; } if (sscanf(tmp, "%d", &ret) != 1 || ret < 1 || ret > nmsgs) { session_printf(s, "{{B}}Invalid message ID{{/B}} (^C to cancel)\r\n"); session_flush(s); xfree(&tmp); goto get_id; } get_id_done: if (tmp != NULL) xfree(&tmp); return ret; } void mail_menu(struct session *s) { static const struct session_menu_option opts[] = { { '#', "#", "Read mail message [#]" }, { '<', "<", "Newer messages" }, { 'l', "Ll", "List mail messages" }, { '>', ">", "Older messages" }, { 'm', "MmNn", "Compose new mail message" }, { 'q', "QqXx", "Return to main menu" }, { '?', "?", "Show this help menu" }, }; static const char prompt_help[] = "#:Read <:Newer >:Older L:List M:Compose Q:Return ?:Help"; size_t nmsgs, nmail_ids; unsigned long *mail_ids = NULL, page, pages; short ret, mpp, mn; char c; bool show_help = false; bool done = false; bool find_message_ids = true; bool show_list = true; if (!s->user) { session_printf(s, "Mail is not available to guests.\r\n" "Please create an account first.\r\n"); session_flush(s); return; } page = 0; while (!done && !s->ending) { if (find_message_ids) { if (mail_ids != NULL) xfree(&mail_ids); mpp = MSGS_PER_PAGE; if (s->terminal_lines < mpp + 3) mpp = BOUND(mpp, 5, s->terminal_lines - 3); nmsgs = mail_find_ids_for_user(s->user, &nmail_ids, &mail_ids, page * mpp, mpp, false); /* ceil(nmsgs / mpp) */ pages = (nmsgs + mpp - 1) / mpp; if (page >= pages) page = pages - 1; find_message_ids = false; } if (show_list) { mail_list(s, nmail_ids, mail_ids, page + 1, pages); show_list = false; } c = session_menu(s, "Private Mail", "Mail", (char *)prompt_help, opts, nitems(opts), show_help, "Message #", &mn); show_help = false; handle_opt: switch (c) { case 'm': mail_compose(s, NULL, NULL, NULL, NULL); break; case 'l': show_list = true; break; case '>': case '<': if (c == '>' && page == pages - 1) { session_printf(s, "You are at the last page of messages\r\n"); session_flush(s); break; } if (c == '<' && page == 0) { session_printf(s, "You are already at the first page\r\n"); session_flush(s); break; } if (c == '>') page++; else page--; find_message_ids = true; show_list = true; break; case '#': if (mn < 1 || mn > nmail_ids) { session_printf(s, "Invalid message\r\n"); session_flush(s); break; } ret = mail_read(s, mail_ids[mn - 1], mn); switch (ret) { case MAIL_READ_RETURN_DONE: break; case MAIL_READ_RETURN_LIST: show_list = true; break; case MAIL_READ_RETURN_FIND: find_message_ids = true; show_list = true; break; default: c = ret; goto handle_opt; } break; case '?': show_help = true; break; default: done = true; break; } } if (mail_ids != NULL) xfree(&mail_ids); } void mail_compose(struct session *s, char *initial_to, char *initial_subject, char *initial_body, char *ftn_reply_msgid) { struct user *to_user = NULL; struct mail_message msg = { 0 }; struct fidopkt_address ftn_our_address, ftn_to; struct fidopkt_message ftnmsg = { 0 }; char *to_username = NULL, *tmp, *at; char c; if (!s->user) { session_printf(s, "Mail is not available to guests.\r\n" "Please create an account first.\r\n"); session_flush(s); return; } fidopkt_parse_address(db->config.ftn_node_addr, &ftn_our_address); if (initial_to) { to_username = xstrdup(initial_to); if (to_username == NULL) goto mail_compose_done; } if (initial_subject) { msg.subject = xstrdup(initial_subject); if (msg.subject == NULL) goto mail_compose_done; } if (initial_body) { msg.body = xstrdup(initial_body); if (msg.body == NULL) goto mail_compose_done; } session_printf(s, "{{B}}Compose New Mail{{/B}}\r\n"); session_printf(s, "{{B}}From: {{/B}} %s\r\n", s->user->username); session_flush(s); mail_compose_start: for (;;) { session_printf(s, "{{B}}To: {{/B}} "); session_flush(s); tmp = session_field_input(s, 50, 50, to_username, false, 0); if (to_username != NULL) xfree(&to_username); to_username = tmp; if (to_username == NULL || to_username[0] == '\0') { session_output(s, "\r\n", 2); session_flush(s); goto mail_compose_done; } if ((at = strstr(to_username, "@")) != NULL) { if (ftn_our_address.zone == 0) { session_printf(s, "\r\n{{B}}Error:{{/B}}{{#}} NetMail " "is not supported at this time.\r\n"); session_flush(s); xfree(&to_username); continue; } if (!fidopkt_parse_address(at + 1, &ftn_to)) { session_printf(s, "\r\n{{B}}Error:{{/B}}{{#}} Invalid " "%s destination \"%s\" (^C to cancel)\r\n", db->config.ftn_network, at + 1); session_flush(s); xfree(&to_username); continue; } memcpy(&msg.ftn_dest, &ftn_to, sizeof(msg.ftn_dest)); strlcpy(msg.ftn_to, to_username, sizeof(msg.ftn_to)); if ((at = strstr(msg.ftn_to, "@")) != NULL) at[0] = '\0'; strlcpy(msg.ftn_from, s->user->username, sizeof(msg.ftn_from)); session_printf(s, " (%s)\r\n", db->config.ftn_network); session_flush(s); } else { session_output(s, "\r\n", 2); session_flush(s); to_user = user_find_by_username(to_username); if (to_user == NULL) { session_printf(s, "{{B}}Error:{{/B}}{{#}} No such user " "\"%s\" (^C to cancel)\r\n", to_username); session_flush(s); xfree(&to_username); continue; } msg.recipient_user_id = to_user->id; xfree(&to_user); } break; } for (;;) { session_printf(s, "{{B}}Subject:{{/B}} "); session_flush(s); tmp = session_field_input(s, 60, 60, msg.subject, false, 0); if (msg.subject != NULL) xfree(&msg.subject); msg.subject = tmp; session_output(s, "\r\n", 2); session_flush(s); if (msg.subject == NULL) goto mail_compose_done; rtrim(msg.subject, "\r\n\t "); if (msg.subject[0] == '\0') { session_printf(s, "{{B}}Error:{{/B}} Subject cannot " "be blank (^C to cancel)\r\n"); session_flush(s); xfree(&msg.subject); continue; } msg.subject_size = strlen(msg.subject) + 1; break; } for (;;) { session_printf(s, "{{B}}Message (^D when finished):{{/B}}\r\n"); session_flush(s); tmp = session_field_input(s, 2048, s->terminal_columns - 1, msg.body, true, 0); if (msg.body != NULL) xfree(&msg.body); msg.body = tmp; session_output(s, "\r\n", 2); session_flush(s); if (msg.body == NULL) goto mail_compose_done; rtrim(msg.body, "\r\n\t "); if (msg.body[0] == '\0') { session_printf(s, "{{B}}Error:{{/B}} Message cannot " "be blank (^C to cancel)\r\n"); session_flush(s); xfree(&msg.body); msg.body = NULL; continue; } msg.body_size = strlen(msg.body) + 1; break; } for (;;) { session_printf(s, "\r\n{{B}}(S){{/B}}end "); if (msg.ftn_to[0]) session_printf(s, "%s ", db->config.ftn_network); session_printf(s, "message, " "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? "); session_flush(s); c = session_input_char(s); if (c == 0 && s->obuflen > 0) { s->node_funcs->output(s); uthread_yield(); if (s->ending) goto mail_compose_done; continue; } session_printf(s, "%c\r\n", c); session_flush(s); switch (c) { case 's': case 'S': case 'y': case '\n': case '\r': /* send */ if (msg.ftn_to[0]) session_printf(s, "Queueing %s NetMail for delivery...", db->config.ftn_network); else session_printf(s, "Sending mail..."); session_flush(s); msg.time = Time; msg.sender_user_id = s->user->id; msg.id = bile_next_id(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE); if (msg.ftn_to[0]) { if (ftn_reply_msgid != NULL && ftn_reply_msgid[0]) strlcpy(msg.ftn_reply, ftn_reply_msgid, sizeof(msg.ftn_reply)); msg.ftn_msgid.id = 0x10FF0000 | ((unsigned long)ftn_our_address.node << 24) | msg.id; msg.ftn_msgid.zone = ftn_our_address.zone; msg.ftn_msgid.net = ftn_our_address.net; msg.ftn_msgid.node = ftn_our_address.node; msg.ftn_msgid.point = ftn_our_address.point; } if (mail_save(&msg) != 0) { session_printf(s, "failed!\r\n"); session_flush(s); break; } if (msg.ftn_to[0]) { ftnmsg.time = msg.time; memcpy(&ftnmsg.header.orig, &ftn_our_address, sizeof(ftnmsg.header.orig)); memcpy(&ftnmsg.header.dest, &msg.ftn_dest, sizeof(ftnmsg.header.dest)); strlcpy(ftnmsg.to, msg.ftn_to, sizeof(ftnmsg.to)); strlcpy(ftnmsg.from, msg.ftn_from, sizeof(ftnmsg.from)); strlcpy(ftnmsg.subject, msg.subject, sizeof(ftnmsg.subject)); ftnmsg.body = msg.body; ftnmsg.body_len = msg.body_size - 1; if (ftn_reply_msgid != NULL && ftn_reply_msgid[0]) strlcpy(ftnmsg.reply, ftn_reply_msgid, sizeof(ftnmsg.reply)); ftnmsg.msgid = msg.ftn_msgid; ftnmsg.attr = FIDOPKT_MSG_ATTR_PRIVATE; if (!binkp_scan_message(&ftnmsg)) { session_printf(s, "failed!\r\n"); session_flush(s); break; } session_printf(s, " done!\r\n"); } else session_printf(s, " sent!\r\n"); session_flush(s); goto mail_compose_done; break; case 'e': case 'E': goto mail_compose_start; case 'c': case 'C': case CONTROL_C: goto mail_compose_done; } } mail_compose_done: if (to_username != NULL) xfree(&to_username); mail_free_message_strings(&msg); } void mail_list(struct session *s, size_t nmail_ids, unsigned long *mail_ids, size_t page, size_t pages) { char time[10], from[40]; size_t n, size; struct mail_message msg; struct username_cache *user; char *data; session_printf(s, "{{B}}Mail (Page %ld of %ld){{/B}}\r\n", page, pages); session_printf(s, "%s# Flg Date From Subject%s\r\n", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); for (n = 0; n < nmail_ids; n++) { size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, mail_ids[n], &data); if (size == 0) break; bile_unmarshall_object(db->mail_bile, mail_object_fields, nitems(mail_object_fields), data, size, &msg, sizeof(msg), true); xfree(&data); if (msg.ftn_from[0] && msg.recipient_user_id) { size = snprintf(from, sizeof(from), "%s@%u:%u/%u", msg.ftn_from, msg.ftn_msgid.zone, msg.ftn_msgid.net, msg.ftn_msgid.node); if (msg.ftn_msgid.point != 0) snprintf(from + size, sizeof(from) - size, ".%u", msg.ftn_msgid.point); } else if (msg.sender_user_id == 0) { strlcpy(from, "(System)", sizeof(from)); } else { user = user_username(msg.sender_user_id); strlcpy(from, user ? user->username : "(unknown)", sizeof(from)); } strftime(time, sizeof(time), "%b %d", localtime(&msg.time)); session_printf(s, "%s%ld %c %s %-15.15s {{#}}%.40s%s\r\n", msg.read ? "" : ansi(s, ANSI_BOLD, ANSI_END), n + 1, msg.read ? ' ' : 'N', time, from, msg.subject, msg.read ? "" : ansi(s, ANSI_RESET, ANSI_END)); mail_free_message_strings(&msg); } session_flush(s); } short mail_read(struct session *s, unsigned long id, short idx) { static const struct session_menu_option opts[] = { { 'r', "Rr", "Reply to this message" }, { 'd', "Dd", "Delete this message" }, { 'u', "Uu", "Mark this message unread" }, { 'm', "MmNn", "Compose new mail message" }, { 'l', "Ll", "Return and list mail messages" }, { 'q', "QqXx", "Return to message list" }, { '?', "?", "Show this help menu" }, }; static const char prompt_help[] = "R:Reply D:Delete U:Unread M:New Msg L:List Q:Return ?:Help"; char time[32]; char prompt[24]; char title[50]; char from[50], to[50]; size_t size; struct mail_message msg; struct username_cache *user; short ret = MAIL_READ_RETURN_DONE; char *data, *reply_subject; bool done = false, show_help = false; char c; size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, id, &data); if (size == 0) { session_printf(s, "{{B}}Error:{{/B}} Can't find message\r\n"); session_flush(s); return MAIL_READ_RETURN_DONE; } bile_unmarshall_object(db->mail_bile, mail_object_fields, nitems(mail_object_fields), data, size, &msg, sizeof(msg), true); xfree(&data); if (msg.ftn_from[0]) { size = snprintf(from, sizeof(from), "%s@%u:%u/%u", msg.ftn_from, msg.ftn_msgid.zone, msg.ftn_msgid.net, msg.ftn_msgid.node); if (msg.ftn_msgid.point) snprintf(from + size, sizeof(from) - size, ".%u", msg.ftn_msgid.point); } else if (msg.sender_user_id == 0) { strlcpy(from, "(System)", sizeof(from)); } else { user = user_username(msg.sender_user_id); if (user) strlcpy(from, user->username, sizeof(from)); else strlcpy(from, "(Unknown)", sizeof(from)); } if (msg.ftn_to[0]) { size = snprintf(to, sizeof(to), "%s@%u:%u/%u", msg.ftn_to, msg.ftn_dest.zone, msg.ftn_dest.net, msg.ftn_dest.node); if (msg.ftn_dest.point) snprintf(to + size, sizeof(to) - size, ".%u", msg.ftn_dest.point); } else { user = user_username(msg.recipient_user_id); if (user) strlcpy(to, user->username, sizeof(to)); else strlcpy(to, "(Unknown)", sizeof(to)); } strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", localtime(&msg.time)); session_printf(s, "{{B}}From:{{/B}} %s\r\n", from); session_printf(s, "{{B}}To:{{/B}} %s\r\n", to); session_printf(s, "{{B}}Date:{{/B}} %s %s\r\n", time, db->config.timezone); session_printf(s, "{{B}}Subject:{{/B}}{{#}} %s\r\n", msg.subject); session_printf(s, "\r\n"); session_flush(s); session_paginate(s, msg.body, msg.body_size, 5); if (!msg.read) { msg.read = Time; if (mail_save(&msg) != 0) session_printf(s, "Failed marking message read!\r\n"); } snprintf(prompt, sizeof(prompt), "Mail:Message %d", idx); snprintf(title, sizeof(title), "Mail: Message %d", idx); while (!done && !s->ending) { c = session_menu(s, title, prompt, (char *)prompt_help, opts, nitems(opts), show_help, NULL, NULL); show_help = false; switch (c) { case 'r': if (msg.sender_user_id == 0) { session_printf(s, "System messages cannot be replied " "to.\r\n"); break; } reply_subject = xmalloc(strlen(msg.subject) + 5); if (reply_subject == NULL) break; if (strncmp(msg.subject, "Re:", 3) == 0) strlcpy(reply_subject, msg.subject, strlen(msg.subject) + 1); else sprintf(reply_subject, "Re: %s", msg.subject); mail_compose(s, from, reply_subject, NULL, msg.ftn_msgid_orig[0] ? msg.ftn_msgid_orig : NULL); xfree(&reply_subject); break; case 'd': if (bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, msg.id, BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE) == 0) session_printf(s, "Deleted message {{B}}%d{{/B}}\r\n", idx); else session_printf(s, "Failed deleting message " "{{B}}%d{{/B}}! (%d)\r\n", idx, bile_error(db->mail_bile)); ret = MAIL_READ_RETURN_FIND; done = true; break; case 'u': msg.read = 0; if (mail_save(&msg) == 0) session_printf(s, "Marked message {{B}}%d{{/B}} unread\r\n", idx); else session_printf(s, "Failed updating message " "{{B}}%d{{/B}}! (%d)\r\n", idx, bile_error(db->mail_bile)); ret = MAIL_READ_RETURN_DONE; done = true; break; case 'm': mail_compose(s, NULL, NULL, NULL, NULL); break; case 'l': done = true; ret = MAIL_READ_RETURN_LIST; break; case 'q': done = true; ret = MAIL_READ_RETURN_DONE; break; case '?': show_help = true; break; } } mail_free_message_strings(&msg); return ret; } short mail_save(struct mail_message *msg) { size_t size; char *data; short ret; if (!msg->id) msg->id = bile_next_id(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE); if (!msg->id) return -1; ret = bile_marshall_object(db->mail_bile, mail_object_fields, nitems(mail_object_fields), msg, &data, &size); if (ret != 0 || size == 0) { warn("mail_save: failed to marshall object"); return -1; } if (bile_write(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, msg->id, data, size) != size) { warn("mail_save: bile_write failed! %d", bile_error(db->mail_bile)); return bile_error(db->mail_bile); } return 0; } /* * return count of all ids for the user (for pagination), set nmail_ids to * the count of the ids set in mail_ids based on offset/limit/only_unread * * nret_mail_ids or ret_mail_ids may be NULL to avoid building an array */ size_t mail_find_ids_for_user(struct user *user, size_t *nret_mail_ids, unsigned long **ret_mail_ids, size_t offset, size_t limit, bool only_unread) { struct mail_message msg; size_t n, nall_mail_ids, nmail_ids, mail_ids_size, nlimit_mail_ids, id, size; bool failed = false; unsigned long *mail_ids = NULL, *all_mail_ids; short i, j; if (ret_mail_ids != NULL) *ret_mail_ids = NULL; if (nret_mail_ids != NULL) *nret_mail_ids = 0; nall_mail_ids = bile_ids_by_type(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, &all_mail_ids); /* narrow all_mail_ids down to mail_ids for this user */ mail_ids_size = 0; nmail_ids = 0; for (n = 0; n < nall_mail_ids; n++) { /* read only as far as we need to */ size = offsetof(struct mail_message, read) + member_size(struct mail_message, read); if (bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, all_mail_ids[n], (char *)&msg, size) != size) { failed = true; goto done; } if (msg.recipient_user_id != user->id) continue; if (only_unread && msg.read) continue; if (ret_mail_ids != NULL) { if (!grow_to_fit(&mail_ids, &mail_ids_size, (nmail_ids + 1) * sizeof(long), sizeof(long), sizeof(long) * 16)) { failed = true; goto done; } mail_ids[nmail_ids] = all_mail_ids[n]; } nmail_ids++; } if (all_mail_ids != NULL) xfree(&all_mail_ids); if (ret_mail_ids != NULL) { /* sort by message id descending for consistent ordering */ for (i = 1; i < nmail_ids; i++) { for (j = i; j > 0; j--) { if (mail_ids[j] < mail_ids[j - 1]) break; id = mail_ids[j]; mail_ids[j] = mail_ids[j - 1]; mail_ids[j - 1] = id; } } } nlimit_mail_ids = nmail_ids; if (offset) { if (offset >= nlimit_mail_ids) { if (mail_ids != NULL) xfree(&mail_ids); nlimit_mail_ids = 0; } else { if (mail_ids != NULL) { for (j = offset, i = 0; j < nmail_ids; j++, i++) mail_ids[i] = mail_ids[j]; } nlimit_mail_ids -= offset; } } if (limit && nlimit_mail_ids > limit) nlimit_mail_ids = limit; if (ret_mail_ids != NULL) *ret_mail_ids = mail_ids; if (nret_mail_ids != NULL) *nret_mail_ids = nlimit_mail_ids; done: if (failed) return 0; return nmail_ids; } short mail_toss_ftn_message(struct fidopkt_message *ftnmsg) { struct user *to = NULL; struct fidopkt_address our_address; size_t n, nmail_ids, size; unsigned long *mail_ids = NULL, sysop_id; struct mail_message msg; short ret = 1; char *data; if (db->config.ftn_node_addr[0] == 0) { logger_printf("[mail] Local FTN node address must be " "configured in settings"); return -1; } if (!fidopkt_parse_address(db->config.ftn_node_addr, &our_address)) { logger_printf("[mail] Failed parsing local FTN node address " "\"%s\", fix in settings", db->config.ftn_node_addr); return -1; } if (memcmp(&ftnmsg->header.dest, &our_address, sizeof(struct fidopkt_address)) != 0) { logger_printf("[mail] NetMail message is destined for " "%u:%u/%u.%u, not us (%u:%u/%u.%u)", ftnmsg->header.dest.zone, ftnmsg->header.dest.net, ftnmsg->header.dest.node, ftnmsg->header.dest.point, our_address.zone, our_address.net, our_address.node, our_address.point); ret = 0; goto done; } to = user_find_by_username(ftnmsg->to); if (to == NULL) { sysop_id = user_first_sysop_id(); if (!sysop_id || !(to = user_find(sysop_id))) { logger_printf("[mail] Can't find sysop username?"); ret = 0; goto done; } logger_printf("[mail] No local user \"%s\", bouncing to sysop %s", ftnmsg->to, to->username); } mail_find_ids_for_user(to, &nmail_ids, &mail_ids, 0, 0, false); for (n = 0; n < nmail_ids; n++) { size = bile_read_alloc(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, mail_ids[n], &data); if (size == 0) break; bile_unmarshall_object(db->mail_bile, mail_object_fields, nitems(mail_object_fields), data, size, &msg, sizeof(msg), false); xfree(&data); if (memcmp(&msg.ftn_msgid, &ftnmsg->msgid, sizeof(struct fidopkt_msgid)) == 0) { logger_printf("[mail] Already have %s NetMail message %s " "(%ld), skipping", db->config.ftn_network, ftnmsg->msgid_orig, msg.id); ret = 0; break; } } if (mail_ids != NULL) xfree(&mail_ids); if (ret < 1) { xfree(&to); return ret; } memset(&msg, 0, sizeof(msg)); msg.recipient_user_id = to->id; msg.time = ftnmsg->time; msg.subject = xstrdup(ftnmsg->subject); msg.subject_size = strlen(ftnmsg->subject) + 1; msg.body = ftnmsg->body; msg.body_size = ftnmsg->body_len + 1; msg.ftn_msgid = ftnmsg->msgid; msg.ftn_orig = ftnmsg->header.orig; msg.ftn_dest = ftnmsg->header.dest; strlcpy(msg.ftn_from, ftnmsg->from, sizeof(msg.ftn_from)); strlcpy(msg.ftn_msgid_orig, ftnmsg->msgid_orig, sizeof(msg.ftn_msgid_orig)); strlcpy(msg.ftn_reply, ftnmsg->reply, sizeof(msg.ftn_reply)); if (mail_save(&msg) != 0) { logger_printf("[binkp] Failed saving %s NetMail message from " "%u:%u/%u.%u to local user %s", db->config.ftn_network, ftnmsg->header.orig.zone, ftnmsg->header.orig.net, ftnmsg->header.orig.node, ftnmsg->header.orig.point, to->username); ret = -1; goto done; } logger_printf("[binkp] Tossed %s NetMail from %u:%u/%u.%u to local " "user %s", db->config.ftn_network, ftnmsg->header.orig.zone, ftnmsg->header.orig.net, ftnmsg->header.orig.node, ftnmsg->header.orig.point, to->username); done: if (to != NULL) xfree(&to); return ret; } void mail_to_sysop(char *subject, char *body) { struct mail_message msg; struct username_cache *ucache; unsigned long sysop_id; sysop_id = user_first_sysop_id(); if (!sysop_id) { logger_printf("[mail] Tried mailing sysop, none found"); return; } memset(&msg, 0, sizeof(msg)); msg.recipient_user_id = sysop_id; msg.time = Time; msg.subject = subject; msg.subject_size = strlen(subject) + 1; msg.body = body; msg.body_size = strlen(body) + 1; if (mail_save(&msg) != 0) { logger_printf("[mail] Failed saving new system mail to sysop"); return; } ucache = user_username(sysop_id); logger_printf("[mail] Sent system mail to %s", ucache ? ucache->username : "(no username)"); } void mail_prune(short days) { struct mail_message msg; size_t n, nids, size, deleted, delta; unsigned long secs, *ids = NULL; if (days < 1) return; secs = (60UL * 60UL * 24UL * (unsigned long)days); logger_printf("[db] Pruning mail older than %d days", days); nids = bile_ids_by_type(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, &ids); if (nids == 0) return; size = offsetof(struct mail_message, time) + member_size(struct mail_message, time); deleted = 0; for (n = 0; n < nids; n++) { if (bile_read(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, ids[n], &msg, size) != size) break; delta = Time - msg.time; if (delta > secs) { deleted++; bile_delete(db->mail_bile, MAIL_SPOOL_MESSAGE_RTYPE, ids[n], 0); uthread_yield(); } } xfree(&ids); if (deleted) { logger_printf("[db] Deleted %ld of %ld mail messages", deleted, nids); bile_write_map(db->mail_bile); } }