From 20cb29c2f8c5c87bc590896854a20b1473ceb358 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Sat, 8 Aug 2026 03:54:55 +0000 Subject: #2 --- src/core/mail.c | 1532 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1532 insertions(+) create mode 100644 src/core/mail.c (limited to 'src/core/mail.c') diff --git a/src/core/mail.c b/src/core/mail.c new file mode 100644 index 0000000..b3aff97 --- /dev/null +++ b/src/core/mail.c @@ -0,0 +1,1532 @@ +#include "hybbx/mail.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/log.h" +#include "mail_sql.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_MAIL_DIR_NAME "mail" +#define HYBBX_MAIL_INBOX_NAME "inbox" +#define HYBBX_MAIL_RECYCLE_NAME "recycle" +#define HYBBX_MAIL_NEXT_FILE "mail.next" + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t read_counter(const char *path, uint64_t *value) +{ + FILE *fp; + unsigned long long n = 0; + + fp = fopen(path, "r"); + if (fp == NULL) { + *value = 0; + return HYBBX_OK; + } + + if (fscanf(fp, "%llu", &n) != 1) { + fclose(fp); + return HYBBX_ERR_IO; + } + + fclose(fp); + *value = (uint64_t)n; + return HYBBX_OK; +} + +static hybbx_result_t write_counter(const char *path, uint64_t value) +{ + FILE *fp; + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%llu\n", (unsigned long long)value); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t mail_user_inbox_path(const hybbx_mail_config_t *mail, + const char *username, + char *out, size_t out_len) +{ + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (mail == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), mail->root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_MAIL_INBOX_NAME); +} + +static hybbx_result_t mail_user_recycle_path(const hybbx_mail_config_t *mail, + const char *username, + char *out, size_t out_len) +{ + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (mail == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), mail->root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_MAIL_RECYCLE_NAME); +} + +static int parse_msg_filename(const char *name, uint64_t *out_id) +{ + char *end; + unsigned long long id; + + if (name == NULL || out_id == NULL) { + return 0; + } + + if (strlen(name) < 5 || strcmp(name + strlen(name) - 4, ".msg") != 0) { + return 0; + } + + id = strtoull(name, &end, 10); + if (end == name || strcmp(end, ".msg") != 0) { + return 0; + } + + *out_id = (uint64_t)id; + return 1; +} + +static int mail_entry_compare(const void *a, const void *b) +{ + const hybbx_mail_entry_t *ea = (const hybbx_mail_entry_t *)a; + const hybbx_mail_entry_t *eb = (const hybbx_mail_entry_t *)b; + + if (ea->received_at > eb->received_at) { + return -1; + } + if (ea->received_at < eb->received_at) { + return 1; + } + if (ea->id > eb->id) { + return -1; + } + if (ea->id < eb->id) { + return 1; + } + return 0; +} + +static hybbx_result_t load_inbox(const char *inbox_path, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + DIR *dir; + struct dirent *ent; + size_t count = 0; + + if (inbox_path == NULL || entries == NULL || out_count == NULL) { + return HYBBX_ERR_INVALID; + } + + *out_count = 0; + + dir = opendir(inbox_path); + if (dir == NULL) { + if (errno == ENOENT) { + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + hybbx_mail_entry_t entry; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (count >= max_entries) { + break; + } + + memset(&entry, 0, sizeof(entry)); + entry.id = id; + + if (hybbx_path_join(path, sizeof(path), inbox_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + continue; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *eq; + char *key; + char *value; + + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + break; + } + + eq = strchr(line, '='); + if (eq == NULL) { + continue; + } + + *eq = '\0'; + key = line; + value = eq + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + + { + size_t vlen = strlen(value); + + while (vlen > 0 && (value[vlen - 1] == '\n' || + value[vlen - 1] == '\r')) { + value[--vlen] = '\0'; + } + } + + if (str_ieq(key, "from")) { + hybbx_strlcpy(entry.from, value, sizeof(entry.from)); + } else if (str_ieq(key, "subject")) { + hybbx_strlcpy(entry.subject, value, sizeof(entry.subject)); + } else if (str_ieq(key, "time")) { + entry.received_at = (time_t)strtol(value, NULL, 10); + } else if (str_ieq(key, "read")) { + entry.read = hybbx_bool_is_true(value); + } + } + + fclose(fp); + entries[count++] = entry; + } + + closedir(dir); + + if (count > 1) { + qsort(entries, count, sizeof(entries[0]), mail_entry_compare); + } + + *out_count = count; + return HYBBX_OK; +} + +static int mail_uses_sqlite(hybbx_service_t *service) +{ + hybbx_storage_t *storage; + + if (service == NULL) { + return 0; + } + + storage = hybbx_service_get_storage(service); + return storage != NULL && + hybbx_storage_backend(storage) == HYBBX_STORAGE_SQLITE; +} + +static hybbx_result_t mail_load_inbox(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + hybbx_storage_t *storage; + char inbox[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (mail_uses_sqlite(service)) { + storage = hybbx_service_get_storage(service); + return hybbx_mail_sql_load_inbox(storage, username, entries, + max_entries, out_count); + } + + rc = mail_user_inbox_path(mail, username, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + return load_inbox(inbox, entries, max_entries, out_count); +} + +static hybbx_result_t msg_file_path(const char *inbox_path, uint64_t id, + char *out, size_t out_len) +{ + char name[32]; + + snprintf(name, sizeof(name), "%06llu.msg", + (unsigned long long)id); + return hybbx_path_join(out, out_len, inbox_path, name); +} + +void hybbx_mail_config_defaults(hybbx_mail_config_t *mail) +{ + if (mail == NULL) { + return; + } + + mail->enabled = 1; + mail->max_messages = HYBBX_MAIL_MAX_MESSAGES; + mail->subject_max = HYBBX_MAIL_SUBJECT_MAX; + mail->body_max = HYBBX_MAIL_BODY_MAX; + mail->recycle_days = HYBBX_MAIL_DEFAULT_RECYCLE_DAYS; + mail->root[0] = '\0'; +} + +void hybbx_mail_config_apply(hybbx_mail_config_t *mail, + const hybbx_config_t *config, + const char *storage_path) +{ + if (mail == NULL) { + return; + } + + hybbx_mail_config_defaults(mail); + + if (config != NULL) { + mail->enabled = hybbx_config_get_bool(config, "mail", "enabled", 1); + mail->max_messages = hybbx_config_get_uint( + config, "mail", "max_messages", HYBBX_MAIL_MAX_MESSAGES, 1u, + HYBBX_MAIL_MAX_MESSAGES); + mail->subject_max = hybbx_config_get_uint( + config, "mail", "subject_max", HYBBX_MAIL_SUBJECT_MAX, 8u, + HYBBX_MAIL_SUBJECT_MAX); + mail->body_max = hybbx_config_get_uint( + config, "mail", "body_max", HYBBX_MAIL_BODY_MAX, 64u, + HYBBX_MAIL_BODY_MAX); + mail->recycle_days = hybbx_config_get_uint( + config, "mail", "recycle_days", HYBBX_MAIL_DEFAULT_RECYCLE_DAYS, + 1u, 365u); + } + + mail->root[0] = '\0'; + if (storage_path != NULL && storage_path[0] != '\0') { + const char *subdir = "mail"; + const char *custom; + + if (config != NULL) { + custom = hybbx_config_get(config, "mail", "path", NULL); + if (custom != NULL && custom[0] != '\0') { + subdir = custom; + } + } + + if (hybbx_path_join(mail->root, sizeof(mail->root), storage_path, + subdir) != HYBBX_OK) { + mail->root[0] = '\0'; + } + } + + hybbx_log_info("[mail] enabled=%s max_messages=%u recycle_days=%u root=%s", + hybbx_bool_to_string(mail->enabled), mail->max_messages, + mail->recycle_days, + mail->root[0] != '\0' ? mail->root : "(unset)"); +} + +static hybbx_result_t mail_ensure_root(const hybbx_mail_config_t *mail) +{ + char next_path[HYBBX_PATH_MAX]; + + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail->root[0] == '\0') { + return HYBBX_ERR_IO; + } + + if (mkdir_p(mail->root) != 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(next_path, sizeof(next_path), mail->root, + HYBBX_MAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + { + FILE *fp = fopen(next_path, "r"); + + if (fp == NULL) { + return write_counter(next_path, 0); + } + fclose(fp); + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_next_id(const hybbx_mail_config_t *mail, + uint64_t *out_id) +{ + char next_path[HYBBX_PATH_MAX]; + uint64_t id; + hybbx_result_t rc; + + if (hybbx_path_join(next_path, sizeof(next_path), mail->root, + HYBBX_MAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + rc = read_counter(next_path, &id); + if (rc != HYBBX_OK) { + return rc; + } + + id++; + rc = write_counter(next_path, id); + if (rc != HYBBX_OK) { + return rc; + } + + *out_id = id; + return HYBBX_OK; +} + +static hybbx_result_t mail_trim_inbox(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username, + unsigned max_messages) +{ + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + size_t i; + hybbx_result_t rc; + char inbox[HYBBX_PATH_MAX]; + + if (mail_uses_sqlite(service)) { + return HYBBX_OK; + } + + rc = mail_user_inbox_path(mail, username, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_inbox(inbox, entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (count <= max_messages) { + return HYBBX_OK; + } + + for (i = max_messages; i < count; i++) { + char path[HYBBX_PATH_MAX]; + + if (msg_file_path(inbox, entries[i].id, path, sizeof(path)) == + HYBBX_OK) { + remove(path); + } + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_ensure_storage(hybbx_service_t *service, + const hybbx_mail_config_t *mail) +{ + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail_uses_sqlite(service)) { + return HYBBX_OK; + } + + return mail_ensure_root(mail); +} + +void hybbx_mail_list_inbox(hybbx_service_t *service, hybbx_session_t *session) +{ + hybbx_mail_list_inbox_range(service, session, 1, 0); +} + +static const char *mail_display_name(hybbx_service_t *service, + const char *name, + char *buf, size_t buf_len) +{ + hybbx_storage_t *storage; + hybbx_user_record_t user; + + if (name == NULL || name[0] == '\0') { + return ""; + } + + storage = hybbx_service_get_storage(service); + if (storage != NULL && + hybbx_storage_resolve_user(storage, name, &user) == HYBBX_OK) { + return hybbx_user_display_name(&user); + } + + hybbx_strlcpy(buf, name, buf_len); + return buf; +} + +static void mail_list_print(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_mail_entry_t *entries, + size_t count, + unsigned from, + unsigned to) +{ + size_t i; + size_t start; + size_t end; + char line[HYBBX_MAIL_SUBJECT_MAX + 64]; + char header[48]; + char from_name[HYBBX_USER_NAME_MAX]; + + if (to == 0 || to > count) { + to = (unsigned)count; + } + + if (count == 0) { + hybbx_session_write_line(session, "Inbox empty."); + hybbx_session_write_line(session, + "Use /mail send to compose."); + return; + } + + if (from == 0 || from > count || from > to) { + hybbx_session_write_line(session, "No messages in that range."); + return; + } + + start = (size_t)(from - 1); + end = (size_t)to; + + if (from == 1 && to == (unsigned)count) { + hybbx_session_write_line(session, "Inbox (newest first):"); + } else { + snprintf(header, sizeof(header), "Inbox %u-%u (newest first):", + from, to); + hybbx_session_write_line(session, header); + } + + for (i = start; i < end; i++) { + snprintf(line, sizeof(line), " %zu %s%s %s", + i + 1, + entries[i].read ? " " : "* ", + mail_display_name(service, entries[i].from, from_name, + sizeof(from_name)), + entries[i].subject[0] != '\0' ? entries[i].subject + : "(no subject)"); + hybbx_session_write_line(session, line); + } + + hybbx_session_write_line(session, + " /mail read delete list recycle"); +} + +static unsigned mail_recycle_purge_expired(const hybbx_mail_config_t *mail, + const char *recycle_path) +{ + DIR *dir; + struct dirent *ent; + time_t now = time(NULL); + time_t max_age; + unsigned removed = 0; + + if (mail == NULL || recycle_path == NULL || recycle_path[0] == '\0') { + return 0; + } + + max_age = (time_t)mail->recycle_days * 86400; + + dir = opendir(recycle_path); + if (dir == NULL) { + if (errno == ENOENT) { + return 0; + } + return 0; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + struct stat st; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (hybbx_path_join(path, sizeof(path), recycle_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + if (stat(path, &st) != 0) { + continue; + } + + if (now - st.st_mtime >= max_age) { + if (remove(path) == 0) { + removed++; + } + } + } + + closedir(dir); + return removed; +} + +static hybbx_result_t mail_move_id_to_recycle(const char *inbox_path, + const char *recycle_path, + uint64_t id) +{ + char src[HYBBX_PATH_MAX]; + char dst[HYBBX_PATH_MAX]; + FILE *in_fp; + FILE *out_fp; + char line[HYBBX_LINE_MAX]; + char tmp[HYBBX_PATH_MAX + 8]; + int has_deleted = 0; + + if (msg_file_path(inbox_path, id, src, sizeof(src)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (mkdir_p(recycle_path) != 0) { + return HYBBX_ERR_IO; + } + + if (msg_file_path(recycle_path, id, dst, sizeof(dst)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (strlen(dst) + 4 >= sizeof(tmp)) { + return HYBBX_ERR_IO; + } + snprintf(tmp, sizeof(tmp), "%s.tmp", dst); + + in_fp = fopen(src, "r"); + out_fp = fopen(tmp, "w"); + if (in_fp == NULL || out_fp == NULL) { + if (in_fp != NULL) { + fclose(in_fp); + } + if (out_fp != NULL) { + fclose(out_fp); + } + remove(tmp); + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), in_fp) != NULL) { + if (strncmp(line, "deleted=", 8) == 0) { + continue; + } + if (!has_deleted && line[0] == '-' && line[1] == '-' && line[2] == '-') { + fprintf(out_fp, "deleted=%ld\n", (long)time(NULL)); + has_deleted = 1; + } + fputs(line, out_fp); + } + + if (!has_deleted) { + fprintf(out_fp, "deleted=%ld\n", (long)time(NULL)); + } + + fclose(in_fp); + fclose(out_fp); + + if (remove(src) != 0) { + remove(tmp); + return HYBBX_ERR_IO; + } + + if (rename(tmp, dst) != 0) { + remove(tmp); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_purge_user_recycle(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username) +{ + char recycle[HYBBX_PATH_MAX]; + + if (mail_uses_sqlite(service)) { + (void)hybbx_mail_sql_purge_recycle(hybbx_service_get_storage(service), + mail, username); + return HYBBX_OK; + } + + if (mail_user_recycle_path(mail, username, recycle, sizeof(recycle)) != + HYBBX_OK) { + return HYBBX_ERR_IO; + } + + (void)mail_recycle_purge_expired(mail, recycle); + return HYBBX_OK; +} + +void hybbx_mail_list_inbox_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + size_t count; + hybbx_result_t rc; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Mail storage unavailable."); + return; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Mail path error."); + return; + } + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Cannot read inbox."); + return; + } + + mail_list_print(service, session, entries, count, from, to); +} + +void hybbx_mail_announce_since_last_login(hybbx_service_t *service, + hybbx_session_t *session, + time_t since_login) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + size_t count; + size_t i; + size_t new_count = 0; + hybbx_result_t rc; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + return; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return; + } + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK || count == 0) { + return; + } + + for (i = 0; i < count; i++) { + if (since_login == 0 || entries[i].received_at > since_login) { + new_count++; + } + } + + if (new_count == 0) { + return; + } + + if (new_count == 1) { + (void)hybbx_msg_send_system(session, + "You have 1 new message since your last login. (/mail)"); + } else { + char body[96]; + snprintf(body, sizeof(body), + "You have %zu new messages since your last login. (/mail)", + new_count); + (void)hybbx_msg_send_system(session, body); + } +} + +int hybbx_mail_parse_list_range(const char *spec, + unsigned *from, unsigned *to) +{ + const char *dash; + char *end; + unsigned long start; + unsigned long end_num; + + if (from == NULL || to == NULL) { + return 0; + } + + if (spec == NULL || spec[0] == '\0') { + *from = 1; + *to = 0; + return 1; + } + + dash = strchr(spec, '-'); + if (dash == NULL) { + start = strtoul(spec, &end, 10); + if (end == spec || *end != '\0' || start == 0) { + return 0; + } + *from = (unsigned)start; + *to = (unsigned)start; + return 1; + } + + start = strtoul(spec, &end, 10); + if (end != dash || start == 0) { + return 0; + } + + end_num = strtoul(dash + 1, &end, 10); + if (end == dash + 1 || *end != '\0' || end_num == 0) { + return 0; + } + + if (start > end_num) { + return 0; + } + + *from = (unsigned)start; + *to = (unsigned)end_num; + return 1; +} + +static hybbx_result_t mail_resolve_index(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index, + uint64_t *out_id, + char *inbox_path, size_t inbox_len) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + hybbx_result_t rc; + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + return rc; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox_path, inbox_len); + if (rc != HYBBX_OK) { + return rc; + } + } + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (list_index == 0 || list_index > count) { + return HYBBX_ERR_NOT_FOUND; + } + + *out_id = entries[list_index - 1].id; + return HYBBX_OK; +} + +static void mail_print_header_line(hybbx_service_t *service, + hybbx_session_t *session, + const char *line) +{ + char out_line[HYBBX_LINE_MAX]; + char name_buf[HYBBX_USER_NAME_MAX]; + char fallback[HYBBX_USER_NAME_MAX]; + const char *value; + const char *display; + size_t vlen; + + if (line == NULL || session == NULL) { + return; + } + + if (strncmp(line, "from=", 5) == 0) { + value = line + 5; + vlen = strlen(value); + while (vlen > 0 && (value[vlen - 1] == '\n' || value[vlen - 1] == '\r')) { + vlen--; + } + memcpy(name_buf, value, vlen); + name_buf[vlen] = '\0'; + display = mail_display_name(service, name_buf, fallback, sizeof(fallback)); + snprintf(out_line, sizeof(out_line), "from=%s", display); + hybbx_session_write_line(session, out_line); + return; + } + + if (strncmp(line, "to=", 3) == 0) { + value = line + 3; + vlen = strlen(value); + while (vlen > 0 && (value[vlen - 1] == '\n' || value[vlen - 1] == '\r')) { + vlen--; + } + memcpy(name_buf, value, vlen); + name_buf[vlen] = '\0'; + display = mail_display_name(service, name_buf, fallback, sizeof(fallback)); + snprintf(out_line, sizeof(out_line), "to=%s", display); + hybbx_session_write_line(session, out_line); + return; + } + + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_mail_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + char inbox[HYBBX_PATH_MAX]; + char path[HYBBX_PATH_MAX]; + uint64_t id; + FILE *fp; + char line[HYBBX_LINE_MAX]; + int in_body = 0; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + if (mail_uses_sqlite(service)) { + (void)hybbx_mail_sql_purge_recycle(hybbx_service_get_storage(service), + hybbx_service_get_mail(service), + hybbx_session_username(session)); + return hybbx_mail_sql_read(service, session, list_index); + } + + (void)mail_purge_user_recycle(service, hybbx_service_get_mail(service), + hybbx_session_username(session)); + + rc = mail_resolve_index(service, session, list_index, &id, + inbox, sizeof(inbox)); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (msg_file_path(inbox, id, path, sizeof(path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + hybbx_session_write_line(session, "Cannot open message."); + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + if (!in_body) { + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + in_body = 1; + continue; + } + if (strncmp(line, "read=", 5) == 0) { + continue; + } + if (strncmp(line, "from=", 5) == 0 || + strncmp(line, "to=", 3) == 0) { + mail_print_header_line(service, session, line); + continue; + } + } + hybbx_session_write_line(session, line); + } + + fclose(fp); + + { + char tmp_path[HYBBX_PATH_MAX + 8]; + FILE *in_fp; + FILE *out_fp; + char buf[HYBBX_LINE_MAX]; + + if (strlen(path) + 4 >= sizeof(tmp_path)) { + return HYBBX_OK; + } + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + in_fp = fopen(path, "r"); + out_fp = fopen(tmp_path, "w"); + if (in_fp != NULL && out_fp != NULL) { + while (fgets(buf, sizeof(buf), in_fp) != NULL) { + if (strncmp(buf, "read=", 5) == 0) { + fputs("read=yes\n", out_fp); + } else { + fputs(buf, out_fp); + } + } + fclose(in_fp); + fclose(out_fp); + remove(path); + rename(tmp_path, path); + } else { + if (in_fp != NULL) { + fclose(in_fp); + } + if (out_fp != NULL) { + fclose(out_fp); + } + remove(tmp_path); + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + char recycle[HYBBX_PATH_MAX]; + size_t count; + size_t i; + unsigned moved = 0; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return HYBBX_ERR_UNSUPPORTED; + } + + if (to == 0) { + to = from; + } + if (from == 0 || from > to) { + hybbx_session_write_line(session, "Usage: /mail delete "); + return HYBBX_OK; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_delete_range(service, session, from, to); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_recycle_path(mail, hybbx_session_username(session), + recycle, sizeof(recycle)); + if (rc != HYBBX_OK) { + return rc; + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (from > count || to > count) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + + for (i = (size_t)(from - 1); i < (size_t)to; i++) { + rc = mail_move_id_to_recycle(inbox, recycle, entries[i].id); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Delete failed."); + return rc; + } + moved++; + } + + if (moved == 1) { + hybbx_session_write_line(session, + "Message moved to recycle (auto-purge after configured days)."); + } else { + char buf[64]; + + snprintf(buf, sizeof(buf), + "%u messages moved to recycle (auto-purge after %u days).", + moved, mail->recycle_days); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_delete(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + return hybbx_mail_delete_range(service, session, list_index, list_index); +} + +hybbx_result_t hybbx_mail_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + const hybbx_mail_config_t *mail; + char recycle[HYBBX_PATH_MAX]; + DIR *dir; + struct dirent *ent; + unsigned removed = 0; + hybbx_result_t rc; + char buf[64]; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_recycle_empty(service, session); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_recycle_path(mail, hybbx_session_username(session), + recycle, sizeof(recycle)); + if (rc != HYBBX_OK) { + return rc; + } + + (void)mail_recycle_purge_expired(mail, recycle); + + dir = opendir(recycle); + if (dir == NULL) { + if (errno == ENOENT) { + hybbx_session_write_line(session, "Recycle bin empty."); + return HYBBX_OK; + } + hybbx_session_write_line(session, "Cannot read recycle bin."); + return HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (hybbx_path_join(path, sizeof(path), recycle, ent->d_name) != + HYBBX_OK) { + continue; + } + + if (remove(path) == 0) { + removed++; + } + } + + closedir(dir); + + if (removed == 0) { + hybbx_session_write_line(session, "Recycle bin empty."); + } else { + snprintf(buf, sizeof(buf), "Recycle bin emptied (%u message(s)).", + removed); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +#define HYBBX_MAIL_SYSTEM_FROM "system" + +static void mail_format_utc_time(time_t when, char *buf, size_t len) +{ + if (buf == NULL || len == 0) { + return; + } + + buf[0] = '\0'; + if (when <= 0) { + return; + } + + { + const struct tm *tm = gmtime(&when); + + if (tm != NULL) { + (void)strftime(buf, len, "%Y-%m-%d %H:%M:%S UTC", tm); + } + } +} + +typedef struct mail_notify_staff_ctx { + hybbx_service_t *service; + char subject[HYBBX_MAIL_SUBJECT_MAX]; + char body[HYBBX_MAIL_BODY_MAX]; +} mail_notify_staff_ctx_t; + +static hybbx_result_t mail_notify_staff_cb(const hybbx_user_record_t *user, + void *ctx) +{ + mail_notify_staff_ctx_t *nctx = (mail_notify_staff_ctx_t *)ctx; + + if (user == NULL || nctx == NULL) { + return HYBBX_OK; + } + + if (!user->active || !hybbx_user_level_is_sysop_or_admin(user->level)) { + return HYBBX_OK; + } + + (void)hybbx_mail_deliver(nctx->service, HYBBX_MAIL_SYSTEM_FROM, + user->username, nctx->subject, nctx->body); + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_notify_staff_registration(hybbx_service_t *service, + const hybbx_user_registration_t *reg, + const hybbx_user_record_t *user) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + mail_notify_staff_ctx_t ctx; + char created[48]; + int n; + + if (service == NULL || reg == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + n = snprintf(ctx.subject, sizeof(ctx.subject), + "Registration pending: %s", reg->nickname); + if (n < 0 || (size_t)n >= sizeof(ctx.subject)) { + return HYBBX_ERR_INVALID; + } + + mail_format_utc_time(user->created_at, created, sizeof(created)); + if (created[0] == '\0') { + hybbx_strlcpy(created, "(unknown)", sizeof(created)); + } + + n = snprintf(ctx.body, sizeof(ctx.body), + "Guest self-registration pending approval.\n" + "/register data submitted:\n\n" + " Nickname: %s\n" + " Login: %s\n" + " Full name: %s\n" + " Country: %s\n" + " Location: %s\n" + " Email: %s\n\n" + "Account record:\n\n" + " User ID: %llu\n" + " Level: %s\n" + " Active: %s\n" + " Created: %s\n\n" + "Review all fields. If correct, activate with:\n" + " /activate %s\n", + reg->nickname, + reg->username, + reg->full_name, + reg->country, + reg->location, + reg->email, + (unsigned long long)user->id, + hybbx_user_level_name(user->level), + hybbx_bool_to_string(user->active), + created, + reg->username); + if (n < 0 || (size_t)n >= sizeof(ctx.body)) { + return HYBBX_ERR_INVALID; + } + + ctx.service = service; + + (void)hybbx_storage_foreach_user(storage, mail_notify_staff_cb, &ctx); + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_deliver(hybbx_service_t *service, + const char *from_user, + const char *to_user, + const char *subject, + const char *body) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + hybbx_user_record_t recipient; + char inbox[HYBBX_PATH_MAX]; + char path[HYBBX_PATH_MAX]; + uint64_t id; + FILE *fp; + hybbx_result_t rc; + char to_norm[HYBBX_USER_NAME_MAX]; + size_t body_len; + + if (service == NULL || from_user == NULL || to_user == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (subject == NULL) { + subject = ""; + } + if (body == NULL) { + body = ""; + } + + body_len = strlen(body); + if (strlen(subject) > mail->subject_max || body_len > mail->body_max) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(to_norm, to_user, sizeof(to_norm)); + hybbx_username_normalize(to_norm); + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, to_user, &recipient); + if (rc == HYBBX_ERR_NOT_FOUND) { + return HYBBX_ERR_NOT_FOUND; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_strlcpy(to_norm, recipient.username, sizeof(to_norm)); + + if (hybbx_user_level_is_guest(recipient.level) || !recipient.active) { + return HYBBX_ERR_DENIED; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_deliver(storage, mail, from_user, to_norm, + subject, body); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_inbox_path(mail, to_norm, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + if (mkdir_p(inbox) != 0) { + return HYBBX_ERR_IO; + } + + rc = mail_next_id(mail, &id); + if (rc != HYBBX_OK) { + return rc; + } + + if (msg_file_path(inbox, id, path, sizeof(path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "id=%llu\n", (unsigned long long)id); + fprintf(fp, "from=%s\n", from_user); + fprintf(fp, "to=%s\n", to_norm); + fprintf(fp, "subject=%s\n", subject); + fprintf(fp, "time=%ld\n", (long)time(NULL)); + fprintf(fp, "read=no\n"); + fprintf(fp, "---\n"); + fputs(body, fp); + if (body_len == 0 || body[body_len - 1] != '\n') { + fputc('\n', fp); + } + fclose(fp); + + (void)mail_trim_inbox(service, mail, to_norm, mail->max_messages); + return HYBBX_OK; +} -- cgit v1.3.1