AmendHub

jcs

/

subtext

/

amendments

/

110

mail: Rewrite, yet again

Implement pagination, move message actions to a sub-menu when viewing
a message by its index on the current page

jcs made amendment 110 8 months ago
--- mail.c Sun May 22 21:56:17 2022 +++ mail.c Tue May 24 22:59:15 2022 @@ -27,6 +27,12 @@ #include "uthread.h" #include "util.h" +#define MSGS_PER_PAGE 10 + +#define MAIL_READ_RETURN_DONE -1 +#define MAIL_READ_RETURN_LIST -2 +#define MAIL_READ_RETURN_FIND -3 + struct bile_object_field mail_object_fields[] = { { offsetof(struct mail_message, recipient_user_id), member_size(struct mail_message, recipient_user_id), -1 }, @@ -53,11 +59,9 @@ size_t nmail_object_fields = nitems(mail_object_fields void mail_free_message_strings(struct mail_message *msg); short mail_save(struct session *s, struct mail_message *msg); -void mail_read(struct session *s, unsigned long idx); -void mail_delete(struct session *s, unsigned long idx); -void mail_mark_unread(struct session *s, unsigned long idx); -void mail_list(struct session *s, bool sent, unsigned long **mail_ids, - size_t *nmsgs); +short mail_read(struct session *s, unsigned long id, short idx); +void mail_list(struct session *s, unsigned long *mail_ids, size_t nmsgs, + size_t page, size_t pages); short mail_get_message_id(struct session *s, size_t nmsgs, char *prompt, short initial); @@ -114,19 +118,23 @@ void mail_menu(struct session *s) { static struct session_menu_option opts[] = { - { 'l', "Ll", "List mail messages" }, { '#', "#0123456789", "Read mail message [#]" }, - { 'd', "Dd", "Delete mail message [#]" }, - { 'u', "Uu", "Mark mail message [#] unread" }, - { 'c', "Cc", "Compose new mail message" }, + { '<', "<", "Previous page of messages" }, + { 'l', "Ll", "List mail messages" }, + { '>', ">", "Next page of messages" }, + { 'm', "MmCc", "Compose new mail message" }, { 'q', "QqXx", "Return to main menu" }, - { '?', "?", "List menu options" }, + { '?', "?", "Show this help menu" }, }; - size_t nmsgs, id; + char title[30]; + size_t nmsgs, nmail_ids, id, page, pages; unsigned long *mail_ids = NULL; + short ret; char c; bool show_help = false; bool done = false; + bool find_message_ids = true; + bool show_list = true; if (!s->user) { session_output_string(s, "Mail is not available to guests.\r\n" @@ -135,59 +143,98 @@ mail_menu(struct session *s) return; } - session_printf(s, "{{B}}Private Mail{{/B}}\r\n"); - session_flush(s); + page = 0; - mail_list(s, false, &mail_ids, &nmsgs); - while (!done) { + if (find_message_ids) { + nmsgs = mail_find_ids_for_user(s->user, &nmail_ids, &mail_ids, + page * MSGS_PER_PAGE, MSGS_PER_PAGE, false); + /* ceil(nmsgs / MSGS_PER_PAGE) */ + pages = (nmsgs + MSGS_PER_PAGE - 1) / MSGS_PER_PAGE; + + if (page >= pages) + page = pages - 1; + + find_message_ids = false; + } + + if (show_list) { + mail_list(s, mail_ids, nmail_ids, page + 1, pages); + show_list = false; + } + c = session_menu(s, "Private Mail", "Mail", opts, nitems(opts), show_help); show_help = false; +handle_opt: switch (c) { - case 'c': + case 'm': mail_compose(s, NULL, NULL, NULL); break; - case 'd': - id = mail_get_message_id(s, nmsgs, "Message # to delete", -1); - if (id != -1) - mail_delete(s, id); - break; case 'l': - mail_list(s, false, &mail_ids, &nmsgs); + show_list = true; break; - case 'u': - id = mail_get_message_id(s, nmsgs, "Message # to mark unread", - -1); - if (id != -1) - mail_mark_unread(s, id); + 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 '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - id = mail_get_message_id(s, nmsgs, "Message # to read", - c - '0'); - if (id != -1) - mail_read(s, mail_ids[id - 1]); + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + if (c >= nmail_ids) { + session_printf(s, "Invalid message\r\n"); + session_flush(s); + break; + } + ret = mail_read(s, mail_ids[c], c); + 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; - case '#': - break; default: done = true; break; } } + + free(mail_ids); } void @@ -206,8 +253,7 @@ mail_compose(struct session *s, char *initial_to, char if (initial_body) msg.body = xstrdup(initial_body); - session_output_template(s, "{{B}}Compose New Private Mail{{/B}}\r\n" - "{{B}}------------{{/B}}\r\n"); + session_output_template(s, "{{B}}Compose New Private Mail{{/B}}\r\n"); session_printf(s, "{{B}}From: {{/B}} %s\r\n", s->user->username); session_flush(s); @@ -314,12 +360,15 @@ mail_compose_start: case '\n': case '\r': /* send */ - session_output_string(s, "Sending mail...\r\n"); + session_output_string(s, "Sending mail..."); session_flush(s); msg.time = Time; msg.sender_user_id = s->user->id; mail_save(s, &msg); + + session_output_string(s, " sent!\r\n"); + session_flush(s); goto mail_compose_done; break; @@ -340,36 +389,37 @@ mail_compose_done: } void -mail_list(struct session *s, bool sent, unsigned long **mail_ids, - size_t *nmsgs) +mail_list(struct session *s, unsigned long *mail_ids, size_t nmsgs, + size_t page, size_t pages) { - char time[24]; + char time[10]; size_t n, size; struct mail_message msg; - struct user_map *user; + struct username_cache *user; char *data; - - *nmsgs = mail_find_for_user_id(s->user->id, mail_ids); - if (*nmsgs == 0) { - session_output_string(s, "No messages.\r\n"); - session_flush(s); - if (*mail_ids) - free(*mail_ids); - return; - } - - for (n = 0; n < *nmsgs; n++) { - size = bile_read_alloc(db->bile, DB_MESSAGE_RTYPE, (*mail_ids)[n], + + session_printf(s, "{{B}}Private 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 < nmsgs; n++) { + size = bile_read_alloc(db->bile, DB_MESSAGE_RTYPE, mail_ids[n], &data); + if (size == 0) + break; bile_unmarshall_object(db->bile, mail_object_fields, nitems(mail_object_fields), data, size, &msg); + free(data); + user = user_find_username(msg.sender_user_id); - strftime(time, sizeof(time), "%m/%d", localtime(&msg.time)); + strftime(time, sizeof(time), "%b %d", localtime(&msg.time)); - session_printf(s, "%s%c [%- 3ld] %s %- 10s {{#}}%- 40s%s\r\n", + session_printf(s, "%s%ld %c %s %- 10s {{#}}%- 40s%s\r\n", msg.read ? "" : ansi(s, ANSI_BOLD, ANSI_END), + n, msg.read ? ' ' : 'N', - n + 1, time, user ? user->username : "(unknown)", msg.subject, @@ -377,22 +427,43 @@ mail_list(struct session *s, bool sent, unsigned long session_flush(s); mail_free_message_strings(&msg); - free(data); } } -void -mail_read(struct session *s, unsigned long id) +short +mail_read(struct session *s, unsigned long id, short idx) { + static struct session_menu_option opts[] = { + { 'r', "Rr", "Reply to this message" }, + { 'd', "Dd", "Delete this message" }, + { 'u', "Uu", "Mark this message unread" }, + { 'l', "Ll", "Return and list mail messages" }, + { '#', "#0123456789", "Return and read mail message [#]" }, + { 'q', "QqXx", "Return to message list" }, + { '?', "?", "Show this help menu" }, + }; char time[32]; + char prompt[24]; + char title[50]; size_t size; struct mail_message msg; - struct user_map *sender, *recipient; - char *data; + struct username_cache *sender, *recipient; + short ret = MAIL_READ_RETURN_DONE; + char *data, *reply_subject; + bool done = false, show_help = false; + char c; size = bile_read_alloc(db->bile, DB_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->bile, mail_object_fields, nitems(mail_object_fields), data, size, &msg); + free(data); + sender = user_find_username(msg.sender_user_id); recipient = user_find_username(msg.recipient_user_id); @@ -403,78 +474,95 @@ mail_read(struct session *s, unsigned long id) sender ? sender->username : "(unknown)"); session_printf(s, "{{B}}To:{{/B}} %s\r\n", recipient ? recipient->username : "(unknown)"); - session_printf(s, "{{B}}Date:{{/B}} %s\r\n", time); + 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_flush(s); session_output_string(s, "\r\n"); - session_output(s, msg.body, msg.body_size); + session_output_string(s, msg.body); session_output_string(s, "\r\n"); - + session_flush(s); + if (!msg.read) { msg.read = Time; if (mail_save(s, &msg) != 0) session_output_string(s, "Failed marking message read!\r\n"); } - mail_free_message_strings(&msg); - free(data); -} + snprintf(prompt, sizeof(prompt), "Mail:Message %d", idx); + snprintf(title, sizeof(title), "Private Mail: Message %d", idx); -void -mail_delete(struct session *s, unsigned long idx) -{ - unsigned long *mail_ids; - size_t nmsgs; - - nmsgs = mail_find_for_user_id(s->user->id, &mail_ids); - if (idx > nmsgs) { - session_printf(s, "Invalid message id {{B}}%ld{{/B}}\r\n", idx); - goto delete_done; + while (!done) { + c = session_menu(s, title, prompt, opts, nitems(opts), + show_help); + show_help = false; + + switch (c) { + case 'r': + if (!sender) + break; + + reply_subject = xmalloc(strlen(msg.subject) + 5); + 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, sender->username, reply_subject, NULL); + + free(reply_subject); + break; + case 'd': + if (bile_delete(db->bile, DB_MESSAGE_RTYPE, msg.id) == 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->bile)); + ret = MAIL_READ_RETURN_FIND; + done = true; + break; + case 'u': + msg.read = 0; + if (mail_save(s, &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->bile)); + ret = MAIL_READ_RETURN_DONE; + done = true; + break; + case 'l': + done = true; + ret = MAIL_READ_RETURN_LIST; + break; + case 'q': + done = true; + ret = MAIL_READ_RETURN_DONE; + break; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + ret = c; + done = true; + break; + case '?': + show_help = true; + break; + } } - if (bile_delete(db->bile, DB_MESSAGE_RTYPE, mail_ids[idx - 1]) == 0) - session_printf(s, "Deleted message {{B}}%ld{{/B}}\r\n", idx); - else - session_printf(s, "Failed deleting message {{B}}%ld{{/B}}! " - "(%d)\r\n", idx, bile_error(db->bile)); - -delete_done: - if (mail_ids) - free(mail_ids); -} - -void -mail_mark_unread(struct session *s, unsigned long idx) -{ - struct mail_message msg; - unsigned long *mail_ids; - size_t nmsgs, size; - char *data; - - nmsgs = mail_find_for_user_id(s->user->id, &mail_ids); - if (idx > nmsgs) { - session_printf(s, "Invalid message id {{B}}%ld{{/B}}\r\n", idx); - goto unread_done; - } - - size = bile_read_alloc(db->bile, DB_MESSAGE_RTYPE, mail_ids[idx - 1], - &data); - bile_unmarshall_object(db->bile, mail_object_fields, - nitems(mail_object_fields), data, size, &msg); - msg.read = 0; - if (mail_save(s, &msg) == 0) - session_printf(s, "Marked message {{B}}%ld{{/B}} unread\r\n", - idx); - else - session_printf(s, "Failed marking message {{B}}%ld{{/B}} " - "unread!\r\n", idx); - mail_free_message_strings(&msg); - free(data); -unread_done: - if (mail_ids) - free(mail_ids); + return ret; } short @@ -504,39 +592,58 @@ mail_save(struct session *s, struct mail_message *msg) } size_t -mail_find_for_user_id(unsigned long user_id, unsigned long **mail_ids) +mail_find_ids_for_user(struct user *user, size_t *nmail_ids, + unsigned long **mail_ids, size_t offset, size_t limit, bool only_unread) { unsigned long msg_user_id; struct bile_object *o; - size_t nmsg, msgs_for_user, mail_ids_size, id; + struct mail_message msg; + size_t n, nmsgs_for_user, nret, mail_ids_size, id, size; short i, j; - - mail_ids_size = sizeof(unsigned long) * 16; + char *data; + bool read; + + mail_ids_size = sizeof(long) * 16; if (mail_ids != NULL) *mail_ids = xmalloc(mail_ids_size); - - msgs_for_user = 0; - nmsg = 0; - while ((o = bile_get_nth_of_type(db->bile, nmsg, DB_MESSAGE_RTYPE))) { - bile_read(db->bile, DB_MESSAGE_RTYPE, o->id, (char *)&msg_user_id, + if (nmail_ids != NULL) + *nmail_ids = 0; + + nmsgs_for_user = 0; + for (n = 0; o = bile_get_nth_of_type(db->bile, n, DB_MESSAGE_RTYPE); + n++) { + id = o->id; + bile_read(db->bile, DB_MESSAGE_RTYPE, id, (char *)&msg_user_id, sizeof(msg_user_id)); - if (msg_user_id == user_id) { - if (mail_ids != NULL) { - EXPAND_TO_FIT(*mail_ids, mail_ids_size, - msgs_for_user * sizeof(long), sizeof(long), - sizeof(unsigned long) * 16); - (*mail_ids)[msgs_for_user] = o->id; - } - msgs_for_user++; - } free(o); - nmsg++; + + if (msg_user_id != user->id) + continue; + + if (only_unread) { + size = bile_read_alloc(db->bile, DB_MESSAGE_RTYPE, id, &data); + bile_unmarshall_object(db->bile, + mail_object_fields, nmail_object_fields, data, size, &msg); + read = msg.read; + mail_free_message_strings(&msg); + if (read) + continue; + } + + if (mail_ids != NULL) { + EXPAND_TO_FIT(*mail_ids, mail_ids_size, + nmsgs_for_user * sizeof(long), sizeof(long), + sizeof(long) * 16); + (*mail_ids)[nmsgs_for_user] = id; + } + + nmsgs_for_user++; } if (mail_ids != NULL) { /* sort by message id for consistent ordering */ - for (i = 0; i < msgs_for_user; i++) { - for (j = 0; j < msgs_for_user - i - 1; j++) { + for (i = 0; i < nmsgs_for_user; i++) { + for (j = 0; j < nmsgs_for_user - i - 1; j++) { if ((*mail_ids)[j] > (*mail_ids)[j + 1]) { id = (*mail_ids)[j]; (*mail_ids)[j] = (*mail_ids)[j + 1]; @@ -546,5 +653,27 @@ mail_find_for_user_id(unsigned long user_id, unsigned } } - return msgs_for_user; + if (nmail_ids != NULL) + *nmail_ids = nmsgs_for_user; + + if (offset) { + if (offset >= nmsgs_for_user) { + if (nmail_ids != NULL) + *nmail_ids = 0; + if (mail_ids != NULL) + free(*mail_ids); + } else { + if (mail_ids != NULL) { + for (j = offset, i = 0; j < nmsgs_for_user; j++, i++) + (*mail_ids)[i] = (*mail_ids)[j]; + } + if (nmail_ids != NULL) + *nmail_ids -= offset; + } + } + + if (nmail_ids != NULL && *nmail_ids > limit) + *nmail_ids = limit; + + return nmsgs_for_user; } --- mail.h Sun May 22 21:24:22 2022 +++ mail.h Tue May 24 16:44:00 2022 @@ -35,11 +35,9 @@ struct mail_message { unsigned long parent_message_id; }; -extern struct bile_object_field mail_object_fields[]; - void mail_menu(struct session *s); void mail_compose(struct session *s, char *to, char *subject, char *body); -size_t mail_find_for_user_id(unsigned long user_id, - unsigned long **mail_ids); +size_t mail_find_ids_for_user(struct user *user, size_t *nmail_ids, + unsigned long **mail_ids, size_t offset, size_t limit, bool only_unread); #endif --- session.c Fri May 20 12:29:38 2022 +++ session.c Tue May 24 16:46:13 2022 @@ -1025,7 +1025,9 @@ session_expand_var(struct session *session, char *ivar session->user->username : GUEST_USERNAME, retsize); } else if (strcmp(var, "new_mail") == 0) { if (session->user) { - if ((mcount = mail_find_for_user_id(session->user->id, NULL))) + mcount = mail_find_ids_for_user(session->user, NULL, + NULL, 0, 0, true); + if (mcount) retlen = sprintf(retval, "(%ld New)", mcount); } } else if (var[0] == '"') { @@ -1237,9 +1239,10 @@ get_menu_option: for (j = 0; ; j++) { if (opt->key[j] == '\0') break; - if (opt->key[j] == c) { - if (opt->ret == '#') - return c; /* return actual number */ + if (opt->ret == '#') { + if (opt->key[j] == c && c != '#') + return c - '0'; /* return actual number */ + } else if (opt->key[j] == c) { return opt->ret; } }