/*! \file mail.cpp * \brief In-game \@mail system. * * This code was taken from Kalkin's DarkZone code, which was * originally taken from PennMUSH 1.50 p10, and has been heavily modified * since being included in MUX. */ #include "copyright.h" #include "autoconf.h" #include "config.h" #include "externs.h" #include "mux_table.h" #include #include #include #include extern "C" { #include "color_ops.h" } const UTF8 *DASH_LINE = T("––––––" "––––––" "––––––" "––––––" "––––––" "––––––" "––––––" "––––––" "––––––" "––––––" "––––––" "––––––" "–––"); const char *MAIL_LINE = "––––––" "––––––" "––––––" "––" " MAIL: %s " "––––––" "––––––" "––––––"; const char *FOLDER_LINE = "––––––" "––––––" "––––––" "––––––" "–––" " MAIL: Folder %d " "––––––" "––––––" "––––––" "––––––" "––––"; #define SIZEOF_MALIAS 13 #define WIDTHOF_MALIASDESC 40 #define SIZEOF_MALIASDESC (WIDTHOF_MALIASDESC*2) struct malias_t { int owner; std::string name; std::string desc; size_t desc_width; // The visual width of the Mail Alias Description. std::vector list; malias_t() : owner(NOTHING), desc_width(0) {} }; static std::vector> malias; static std::vector mail_list; // Per-player mail lists using STL for ownership and iteration. static std::unordered_map> mail_storage; // #1191 softcode gen-sync (defined with mail_fetch). // static void ensure_mail_softcode_sync(void); // Small helper to reduce reinterpret_cast noise when passing std::string // contents to UTF8*-taking APIs (UTF8 is typically unsigned char*). static inline const UTF8* utf8(const std::string& s) { return reinterpret_cast(s.c_str()); } // --------------------------------------------------------------------------- // SQLite write-through helpers for mail mutations. // --------------------------------------------------------------------------- #include "sqlite_backend.h" #define SQLITE_MAIL_WRITABLE() (!mudstate.bSQLiteLoading) static void sqlite_wt_insert_mail(struct mail *mp) { if (!SQLITE_MAIL_WRITABLE()) return; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); mp->sqlite_id = sqldb.InsertMailHeaderReturningId( mp->to, mp->from, mp->number, utf8(mp->tolist), utf8(mp->time), utf8(mp->subject), mp->read); if (mp->sqlite_id < 0) { Log.tinyprintf(T("mail sqlite_wt_insert_mail failed for to=#%d from=#%d body=%d" ENDLINE), mp->to, mp->from, mp->number); } } static void sqlite_wt_update_mail_flags(struct mail *mp) { if (!SQLITE_MAIL_WRITABLE() || mp->sqlite_id < 0) return; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.UpdateMailReadFlags(mp->sqlite_id, mp->read)) { Log.tinyprintf(T("mail sqlite_wt_update_mail_flags failed for rowid=%lld" ENDLINE), static_cast(mp->sqlite_id)); } } static void sqlite_wt_delete_mail(struct mail *mp) { if (!SQLITE_MAIL_WRITABLE() || mp->sqlite_id < 0) return; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.DeleteMailHeader(mp->sqlite_id)) { Log.tinyprintf(T("mail sqlite_wt_delete_mail failed for rowid=%lld" ENDLINE), static_cast(mp->sqlite_id)); } } static void sqlite_wt_delete_all_mail(int to_player) { if (!SQLITE_MAIL_WRITABLE()) return; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.DeleteAllMailHeaders(to_player)) { Log.tinyprintf(T("mail sqlite_wt_delete_all_mail failed for to=#%d" ENDLINE), to_player); } } static void sqlite_wt_mail_body(int number, const UTF8 *message) { if (!SQLITE_MAIL_WRITABLE()) return; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.SyncMailBody(number, message)) { Log.tinyprintf(T("mail sqlite_wt_mail_body failed for body=%d" ENDLINE), number); } // Keep the loader's gate current: sqlite_load_mail() only reads the // mail tables when the mail_db_top meta exists (it sizes the body // array), but until now only a DbConvert -m import set it -- mail // sent in-game wrote through to the tables and was then ignored on // the next boot (fallback to the legacy mail.db flatfile). (#783) // if (!sqldb.PutMeta("mail_db_top", mudstate.mail_db_top)) { Log.tinyprintf(T("mail sqlite_wt_mail_body: mail_db_top meta update failed" ENDLINE)); } } static void sqlite_wt_delete_mail_body(int number) { if (!SQLITE_MAIL_WRITABLE()) return; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.DeleteMailBody(number)) { Log.tinyprintf(T("mail sqlite_wt_delete_mail_body failed for body=%d" ENDLINE), number); } } static void sqlite_wt_sync_all_aliases(void) { if (!SQLITE_MAIL_WRITABLE()) return; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.ClearMailAliases()) { Log.WriteString(T("mail sqlite_wt_sync_all_aliases failed to clear aliases." ENDLINE)); return; } for (size_t i = 0; i < malias.size(); i++) { malias_t *m = malias[i].get(); LBuf members_buf = LBuf_Src("wt_sync_alias"); UTF8 *bp = members_buf; for (size_t j = 0; j < m->list.size(); j++) { if (j > 0) { safe_chr(' ', members_buf, &bp); } safe_str(tprintf(T("%d"), m->list[j]), members_buf, &bp); } *bp = '\0'; if (!sqldb.SyncMailAlias(m->owner, utf8(m->name), utf8(m->desc), static_cast(m->desc_width), members_buf)) { Log.tinyprintf(T("mail sqlite_wt_sync_all_aliases failed for owner=#%d alias=%s" ENDLINE), m->owner, m->name.c_str()); } } } // Handling functions for the database of mail messages. // // mail_db_grow - We keep a database of mail text, so if we send a // message to more than one player, we won't have to duplicate the // text. Now implemented with std::vector + std::string. // // Largest message index we will honor. Message numbers come from the (admin- // supplied, possibly corrupt) mail database; an absurd value would drive a // wild index. No real mail database approaches this. // static constexpr int MAIL_DB_LIMIT = 0x04000000; // 67,108,864 // True if number is a safe index into the (currently sized) mail_list. // static inline bool mail_index_valid(int number) { return 0 <= number && number < mudstate.mail_db_top; } static void mail_db_grow(int newtop) { if (newtop <= mudstate.mail_db_top) { return; } if (newtop > MAIL_DB_LIMIT) { STARTLOG(LOG_PROBLEMS, "MAIL", "SIZE"); log_printf(T("mail_db_grow: refusing absurd mail size %d."), newtop); ENDLOG; return; } // With vector, just resize. Existing entries are preserved. // No fudge, no manual alloc/copy. if (static_cast(mail_list.size()) < newtop) { mail_list.resize(newtop); } // New entries are default-constructed (refs=0, empty string). mudstate.mail_db_top = newtop; } // MessageReferenceInc - Increments the reference count for any // particular message. // static inline void MessageReferenceInc(int number) { if (!mail_index_valid(number)) { return; } mail_list[number].m_nRefs++; } // MessageReferenceCheck - Checks whether the reference count for // any particular message indicates that the message body should be // freed. Also checks that if a message pointer is null, that the // reference count is zero. // static void MessageReferenceCheck(int number) { if (!mail_index_valid(number)) { return; } MAILBODY &m = mail_list[number]; if (m.m_nRefs <= 0) { m.m_pMessage.clear(); } if (m.m_pMessage.empty()) { m.m_nRefs = 0; } } // MessageReferenceDec - Decrements the reference count for a message, and // will also delete the message if the counter reaches 0. // static void MessageReferenceDec(int number) { if (!mail_index_valid(number)) { return; } mail_list[number].m_nRefs--; if (mail_list[number].m_nRefs <= 0) { sqlite_wt_delete_mail_body(number); } MessageReferenceCheck(number); } // MessageFetch - returns the text for a particular message number. This // text should not be modified. // const UTF8 *MessageFetch(int number) { if (!mail_index_valid(number)) { return M_("MAIL: This mail message does not exist in the database. Please alert your admin."); } MessageReferenceCheck(number); if (!mail_list[number].m_pMessage.empty()) { return utf8(mail_list[number].m_pMessage); } else { return M_("MAIL: This mail message does not exist in the database. Please alert your admin."); } } size_t MessageFetchSize(int number) { if (!mail_index_valid(number)) { return 0; } MessageReferenceCheck(number); return mail_list[number].m_pMessage.size(); } // This function returns a reference to the message and the the // reference count is increased to reflect that. // static int MessageAdd(UTF8 *pMessage) { size_t len = strlen(reinterpret_cast(pMessage)); int i; bool bFound = false; for (i = 0; i < mudstate.mail_db_top; i++) { if (mail_list[i].m_pMessage.empty()) { mail_list[i].m_nRefs = 0; bFound = true; break; } } if (!bFound) { mail_db_grow(i + 1); } MAILBODY &pm = mail_list[i]; pm.m_pMessage.assign(reinterpret_cast(pMessage), len); MessageReferenceInc(i); sqlite_wt_mail_body(i, pMessage); return i; } // add_mail_message - adds a new text message to the mail database, and returns // a unique number for that message. // // IF return value is !NOTHING, you have a reference to the message, // and the reference count reflects that. // static int add_mail_message(dbref player, UTF8 *message) { if (!mux_stricmp(message, T("clear"))) { raw_notify(player, M_("MAIL: You probably did not intend to send a @mail saying ‘clear’.")); return NOTHING; } // Evaluate signature. // int aflags; dbref aowner; UTF8 *bp = alloc_lbuf("add_mail_message"); UTF8 *atrstr = atr_get("add_mail_message.216", player, A_SIGNATURE, &aowner, &aflags); UTF8 *execstr = bp; mux_exec(atrstr, LBUF_SIZE-1, execstr, &bp, player, player, player, AttrTrace(aflags, EV_STRIP_CURLY|EV_FCHECK|EV_EVAL), nullptr, 0); *bp = '\0'; // Save message body and return a reference to it. // // Join with a space only when there IS a signature (#1587). The // unconditional "%s %s" appended a trailing space to every message sent // by a player with no A_SIGNATURE -- which is most of them -- so a // 32-character body read back as 33, and mailinfo(...,size,...) // faithfully reported the inflated length. The comsys module already // guards this; the engine did not, and the module was the correct one. // int number = ('\0' != execstr[0]) ? MessageAdd(tprintf(T("%s %s"), message, execstr)) : MessageAdd(message); free_lbuf(atrstr); free_lbuf(execstr); return number; } // This function is -only- used from reading from the disk, and so // it does -not- manage the reference counts. // static bool MessageAddWithNumber(int i, UTF8 *pMessage) { // i comes from the (possibly corrupt) mail database. Reject negative and // absurd values, and bail if the grow did not actually make room. // if (i < 0 || MAIL_DB_LIMIT < i) { return false; } mail_db_grow(i+1); if (!mail_index_valid(i)) { return false; } size_t len = strlen(reinterpret_cast(pMessage)); MAILBODY &pm = mail_list[i]; pm.m_pMessage.assign(reinterpret_cast(pMessage), len); return true; } // new_mail_message - used for reading messages in from disk which // already have a number assigned to them. // // This function is -only- used from reading from the disk, and so // it does -not- manage the reference counts. // static void new_mail_message(UTF8 *message, int number) { bool bTruncated = false; if (strlen(reinterpret_cast(message)) > LBUF_SIZE-1) { bTruncated = true; message[LBUF_SIZE-1] = '\0'; } MessageAddWithNumber(number, message); if (bTruncated) { STARTLOG(LOG_BUGS, "BUG", "MAIL"); log_printf(T("new_mail_message: Mail message %d truncated."), number); ENDLOG; } } /*-------------------------------------------------------------------------* * User mail functions (these are called from game.c) * * do_mail - cases * do_mail_read - read messages * do_mail_list - list messages * do_mail_flags - tagging, untagging, clearing, unclearing of messages * do_mail_file - files messages into a new folder * do_mail_fwd - forward messages to another player(s) * do_mail_reply - reply to a message * do_mail_count - count messages * do_mail_purge - purge cleared messages * do_mail_change_folder - change current folder *-------------------------------------------------------------------------*/ static void set_player_folder(dbref player, int fnum) { // Set a player's folder to fnum. // UTF8 *tbuf1 = alloc_sbuf("set_player_folder"); mux_ltoa(fnum, tbuf1); ATTR *a = atr_num(A_MAILCURF); if (a) { atr_add(player, A_MAILCURF, tbuf1, GOD, a->flags); } else { // Shouldn't happen, but... // atr_add(player, A_MAILCURF, tbuf1, GOD, AF_ODARK | AF_WIZARD | AF_NOPROG | AF_LOCK); } free_sbuf(tbuf1); } static void add_folder_name(dbref player, int fld, UTF8 *name) { // Fetch current list of folders // int aflags; size_t nFolders; dbref aowner; UTF8 *aFolders = alloc_lbuf("add_folder_name.str"); atr_get_str_LEN(aFolders, player, A_MAILFOLDERS, &aowner, &aflags, &nFolders); // Build new record ("%d:%s:%d", fld, uppercase(name), fld) upper-casing // the provided folder name. // UTF8 *aNew = alloc_lbuf("add_folder_name.new"); mux_sprintf(aNew, LBUF_SIZE, T("%d:%s:%d"), fld, name, fld); size_t nNew = strlen(reinterpret_cast(aNew)); { LBuf tmp = LBuf_Src("add_folder_name"); nNew = co_toupper(tmp, aNew, nNew); memcpy(aNew, tmp, nNew + 1); } UTF8 *p, *q; if (0 != nFolders) { // Build pattern ("%d:", fld) // UTF8 *aPattern = alloc_lbuf("add_folder_name.pat"); q = aPattern; q += mux_ltoa(fld, q); safe_chr(':', aPattern, &q); *q = '\0'; size_t nPattern = q - aPattern; BMH_State bmhs; BMH_Prepare(&bmhs, nPattern, aPattern); for (;;) { size_t i; if (!BMH_Execute(&bmhs, &i, nPattern, aPattern, nFolders, aFolders)) { break; } // Remove old record. // q = aFolders + i; p = q + nPattern; // Eat leading spaces. // while ( aFolders < q && mux_isspace(q[-1])) { q--; } // Skip past old record and trailing spaces. // while ( *p && *p != ':') { p++; } while ( *p && !mux_isspace(*p)) { p++; } while (mux_isspace(*p)) { p++; } if (q != aFolders) { *q++ = ' '; } while (*p) { safe_chr(*p, aFolders, &q); p++; } *q = '\0'; nFolders = q - aFolders; } free_lbuf(aPattern); } if (nFolders + 1 + nNew < LBUF_SIZE) { // It will fit. Append new record. // q = aFolders + nFolders; if (nFolders) { *q++ = ' '; } memcpy(q, aNew, nNew); q += nNew; *q = '\0'; atr_add(player, A_MAILFOLDERS, aFolders, player, AF_MDARK | AF_WIZARD | AF_NOPROG | AF_LOCK); } free_lbuf(aFolders); free_lbuf(aNew); } static const UTF8 *get_folder_name(dbref player, int fld) { // Get the name of the folder, or return "unnamed". // int aflags; size_t nFolders; dbref aowner; thread_local UTF8 aFolders[LBUF_SIZE]; atr_get_str_LEN(aFolders, player, A_MAILFOLDERS, &aowner, &aflags, &nFolders); if (nFolders != 0) { UTF8 *aPattern = alloc_lbuf("get_folder_name"); UTF8 *p = aPattern; p += mux_ltoa(fld, p); *p++ = ':'; *p = '\0'; size_t nPattern = p - aPattern; size_t i; bool bSucceeded = BMH_StringSearch(&i, nPattern, aPattern, nFolders, aFolders); free_lbuf(aPattern); if (bSucceeded) { UTF8 *pFolder = aFolders + i + nPattern; UTF8 *q = pFolder; while ( *q && *q != ':') { q++; } *q = '\0'; return pFolder; } } return T("unnamed"); } static int get_folder_number(dbref player, UTF8 *name) { // Look up a folder name and return the corresponding folder number. // int aflags; size_t nFolders; dbref aowner; UTF8 *aFolders = alloc_lbuf("get_folder_num_str"); atr_get_str_LEN(aFolders, player, A_MAILFOLDERS, &aowner, &aflags, &nFolders); if (nFolders != 0) { // Convert the folder name provided into upper-case characters. // UTF8 *aPattern = alloc_lbuf("add_folder_num_pat"); mux_sprintf(aPattern, LBUF_SIZE, T(":%s:"), name); size_t nPattern = strlen(reinterpret_cast(aPattern)); { LBuf tmp = LBuf_Src("get_folder_number"); nPattern = co_toupper(tmp, aPattern, nPattern); memcpy(aPattern, tmp, nPattern + 1); } size_t i; bool bSucceeded = BMH_StringSearch(&i, nPattern, aPattern, nFolders, aFolders); free_lbuf(aPattern); UTF8 *p, *q; if (bSucceeded) { p = aFolders + i + nPattern; q = p; while ( *q && !mux_isspace(*q)) { q++; } *q = '\0'; // A bug in TinyMUX 2.7 (Jan 22, 2008 through Feb 1, 2009) // generated a leading '#' in folder numbers. The following // workaround can eventually be removed. // if ('#' == *p) { p++; } int64_t iFolderNumber = mux_atoi64(p); free_lbuf(aFolders); return iFolderNumber; } } free_lbuf(aFolders); return -1; } static int parse_folder(dbref player, UTF8 *folder_string) { // Given a string, return a folder #, or -1. // if ( !folder_string || !*folder_string) { return -1; } if (mux_isdigit(*folder_string)) { int64_t fnum = mux_atoi64(folder_string); if ( fnum < 0 || fnum > MAX_FOLDERS) { return -1; } else { return fnum; } } // Handle named folders here // return get_folder_number(player, folder_string); } #define MAIL_INVALID_RANGE 0 #define MAIL_INVALID_NUMBER 1 #define MAIL_INVALID_AGE 2 #define MAIL_INVALID_DBREF 3 #define MAIL_INVALID_PLAYER 4 #define MAIL_INVALID_SPEC 5 #define MAIL_INVALID_PLAYER_OR_USING_MALIAS 6 // Player-facing @mail errors. N_() marks for xgettext; mail_msg() looks up // via mux_gettext at use so a static table does not need a refresh pass. // static const UTF8 *mailmsg[] = { N_("MAIL: Invalid message range"), N_("MAIL: Invalid message number"), N_("MAIL: Invalid age"), N_("MAIL: Invalid dbref #"), N_("MAIL: Invalid player"), N_("MAIL: Invalid message specification"), N_("MAIL: Invalid player or trying to send @mail to a @malias without a subject"), }; static inline const UTF8 *mail_msg(int i) { #if defined(HAVE_NLS) return mux_gettext(mailmsg[i]); #else return mailmsg[i]; #endif } static bool parse_msglist(UTF8 *msglist, struct mail_selector *ms, dbref player) { // Take a message list, and return the appropriate mail_selector setup. // For now, msglists are quite restricted. That'll change once all this // is working. Returns 0 if couldn't parse, and also notifies the player // why. // Initialize the mail selector - this matches all messages. // ms->low = 0; ms->high = 0; ms->flags = 0x0FFF | M_MSUNREAD; ms->player = 0; ms->days = -1; ms->day_comp = 0; // Now, parse the message list. // if (!msglist || !*msglist) { // All messages // return true; } UTF8 *p = msglist; while (mux_isspace(*p)) { p++; } if (*p == '\0') { return true; } if (mux_isdigit(*p)) { // Message or range. // UTF8 *q = reinterpret_cast(strchr(reinterpret_cast(p), '-')); if (q) { // We have a subrange, split it up and test to see if it is valid. // q++; ms->low = mux_atoi64(p); if (ms->low <= 0) { raw_notify(player, mail_msg(MAIL_INVALID_RANGE)); return false; } if (*q == '\0') { // Unbounded range. // ms->high = 0; } else { ms->high = mux_atoi64(q); if (ms->low > ms->high) { raw_notify(player, mail_msg(MAIL_INVALID_RANGE)); return false; } } } else { // A single message. // ms->low = ms->high = mux_atoi64(p); if (ms->low <= 0) { raw_notify(player, mail_msg(MAIL_INVALID_NUMBER)); return false; } } } else { switch (*p) { case '-': // Range with no start. // p++; if (*p == '\0') { raw_notify(player, mail_msg(MAIL_INVALID_RANGE)); return false; } ms->high = mux_atoi64(p); if (ms->high <= 0) { raw_notify(player, mail_msg(MAIL_INVALID_RANGE)); return false; } break; case '~': // Exact # of days old. // p++; if (*p == '\0') { raw_notify(player, mail_msg(MAIL_INVALID_AGE)); return false; } ms->day_comp = 0; ms->days = mux_atoi64(p); if (ms->days < 0) { raw_notify(player, mail_msg(MAIL_INVALID_AGE)); return false; } break; case '<': // Less than # of days old. // p++; if (*p == '\0') { raw_notify(player, mail_msg(MAIL_INVALID_AGE)); return false; } ms->day_comp = -1; ms->days = mux_atoi64(p); if (ms->days < 0) { raw_notify(player, mail_msg(MAIL_INVALID_AGE)); return false; } break; case '>': // Greater than # of days old. // p++; if (*p == '\0') { raw_notify(player, mail_msg(MAIL_INVALID_AGE)); return false; } ms->day_comp = 1; ms->days = mux_atoi64(p); if (ms->days < 0) { raw_notify(player, mail_msg(MAIL_INVALID_AGE)); return false; } break; case '#': // From db#. // p++; if (*p == '\0') { raw_notify(player, mail_msg(MAIL_INVALID_DBREF)); return false; } ms->player = mux_atoi64(p); if (!Good_obj(ms->player) || !(ms->player)) { raw_notify(player, mail_msg(MAIL_INVALID_DBREF)); return false; } break; case '*': // From player name. // p++; if (*p == '\0') { raw_notify(player, mail_msg(MAIL_INVALID_PLAYER)); return false; } ms->player = lookup_player(player, p, true); if (ms->player == NOTHING) { raw_notify(player, mail_msg(MAIL_INVALID_PLAYER_OR_USING_MALIAS)); return false; } break; case 'a': case 'A': // All messages, all folders // p++; switch (*p) { case '\0': raw_notify(player, M_("MAIL: A isn’t enough (all?)")); return false; case 'l': case 'L': // All messages, all folders // p++; switch (*p) { case '\0': raw_notify(player, M_("MAIL: AL isn’t enough (all?)")); return false; case 'l': case 'L': // All messages, all folders // p++; if (*p == '\0') { ms->flags = M_ALL; } else { raw_notify(player, mail_msg(MAIL_INVALID_SPEC)); return false; } break; default: // Bad // raw_notify(player, mail_msg(MAIL_INVALID_SPEC)); return false; } break; default: // Bad // raw_notify(player, mail_msg(MAIL_INVALID_SPEC)); return false; } break; case 'u': case 'U': // Urgent, Unread // p++; if (*p == '\0') { raw_notify(player, M_("MAIL: U is ambiguous (urgent or unread?)")); return false; } switch (*p) { case 'r': case 'R': // Urgent // ms->flags = M_URGENT; break; case 'n': case 'N': // Unread // ms->flags = M_MSUNREAD; break; default: // Bad // raw_notify(player, mail_msg(MAIL_INVALID_SPEC)); return false; } break; case 'r': case 'R': // Read // ms->flags = M_ISREAD; break; case 'c': case 'C': // Cleared. // ms->flags = M_CLEARED; break; case 't': case 'T': // Tagged. // ms->flags = M_TAG; break; case 'm': case 'M': // Mass, me. // p++; if (*p == '\0') { raw_notify(player, M_("MAIL: M is ambiguous (mass or me?)")); return false; } switch (*p) { case 'a': case 'A': ms->flags = M_MASS; break; case 'e': case 'E': ms->player = player; break; default: raw_notify(player, mail_msg(MAIL_INVALID_SPEC)); return false; } break; default: // Bad news. // raw_notify(player, mail_msg(MAIL_INVALID_SPEC)); return false; } } return true; } static int player_folder(dbref player) { // Return the player's current folder number. If they don't have one, set // it to 0. // int flags; UTF8 *atrstr = atr_pget(player, A_MAILCURF, &player, &flags); if (!*atrstr) { free_lbuf(atrstr); set_player_folder(player, 0); return 0; } int64_t number = mux_atoi64(atrstr); free_lbuf(atrstr); return number; } // List mail stats for all current folders // static void DoListMailBrief(dbref player) { for (int folder = 0; folder < MAX_FOLDERS; folder++) { check_mail(player, folder, true); // Show named but empty folders that check_mail skips. // int rc, uc, cc; count_mail(player, folder, &rc, &uc, &cc); if ( rc + uc == 0 && cc == 0) { const UTF8 *fname = get_folder_name(player, folder); if (strcmp(reinterpret_cast(fname), "unnamed") != 0) { raw_notify(player, tprintf( M_("MAIL: 0 messages in folder %d [%s]."), folder, fname)); } } } int current_folder = player_folder(player); raw_notify(player, tprintf(M_("MAIL: Current folder is %d [%s]."), current_folder, get_folder_name(player, current_folder))); } // Change or rename a folder // static void do_mail_change_folder(dbref player, UTF8 *fld, UTF8 *newname) { int pfld; if (!fld || !*fld) { // Check mail in all folders // DoListMailBrief(player); return; } pfld = parse_folder(player, fld); if (pfld < 0) { raw_notify(player, M_("MAIL: What folder is that?")); return; } if (newname && *newname) { // We're changing a folder name here // if (strlen(reinterpret_cast(newname)) > FOLDER_NAME_LEN) { raw_notify(player, M_("MAIL: Folder name too long")); return; } UTF8 *p; for (p = newname; mux_isalnum(*p); p++) ; if (*p != '\0') { raw_notify(player, M_("MAIL: Illegal folder name")); return; } add_folder_name(player, pfld, newname); raw_notify(player, tprintf(M_("MAIL: Folder %d now named ‘%s’"), pfld, newname)); } else { // Set a new folder // set_player_folder(player, pfld); raw_notify(player, tprintf(M_("MAIL: Current folder set to %d [%s]."), pfld, get_folder_name(player, pfld))); } } static int sign(int x) { if (x == 0) { return 0; } else if (x < 0) { return -1; } else { return 1; } } static bool mail_match(struct mail *mp, struct mail_selector ms, int num) { // Does a piece of mail match the mail_selector? // if (ms.low && num < ms.low) { return false; } if (ms.high && ms.high < num) { return false; } if (ms.player && mp->from != ms.player) { return false; } mail_flag mpflag = Read(mp) ? (mp->read | M_ALL) : (mp->read | M_ALL | M_MSUNREAD); if ((ms.flags & mpflag) == 0) { return false; } if (ms.days == -1) { return true; } // Get the time now, subtract mp->time, and compare the results with // ms.days (in manner of ms.day_comp) // CLinearTimeAbsolute ltaNow; ltaNow.GetLocal(); const UTF8 *pMailTimeStr = utf8(mp->time); CLinearTimeAbsolute ltaMail; if (ltaMail.SetString(pMailTimeStr)) { CLinearTimeDelta ltd(ltaMail, ltaNow); int iDiffDays = ltd.ReturnDays(); if (sign(iDiffDays - ms.days) == ms.day_comp) { return true; } } return false; } // Adjust the flags of a set of messages. // If negate is true, clear the flag. static void do_mail_flags(dbref player, UTF8 *msglist, mail_flag flag, bool negate) { struct mail_selector ms; if (!parse_msglist(msglist, &ms, player)) { return; } int i = 0, j = 0; int folder = player_folder(player); MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( All(ms) || Folder(mp) == folder) { i++; if (mail_match(mp, ms, i)) { j++; if (negate) { mp->read &= ~flag; } else { mp->read |= flag; } sqlite_wt_update_mail_flags(mp); switch (flag) { case M_TAG: raw_notify(player, tprintf(M_("MAIL: Msg #%d %s."), i, negate ? "untagged" : "tagged")); break; case M_CLEARED: if (Unread(mp) && !negate) { raw_notify(player, tprintf(M_("MAIL: Unread Msg #%d cleared! Use @mail/unclear %d to recover."), i, i)); } else { raw_notify(player, tprintf(M_("MAIL: Msg #%d %s."), i, negate ? "uncleared" : "cleared")); } break; case M_SAFE: raw_notify(player, tprintf(M_("MAIL: Msg #%d %s."), i, negate ? "marked unsafe" : "marked safe")); break; } } } } if (!j) { // Ran off the end of the list without finding anything. // raw_notify(player, M_("MAIL: You don’t have any matching messages!")); } } static void do_mail_tag(dbref player, UTF8 *msglist) { do_mail_flags(player, msglist, M_TAG, false); } static void do_mail_safe(dbref player, UTF8 *msglist) { do_mail_flags(player, msglist, M_SAFE, false); } static void do_mail_unsafe(dbref player, UTF8 *msglist) { do_mail_flags(player, msglist, M_SAFE, true); } void do_mail_clear(dbref player, UTF8 *msglist) { do_mail_flags(player, msglist, M_CLEARED, false); } static void do_mail_untag(dbref player, UTF8 *msglist) { do_mail_flags(player, msglist, M_TAG, true); } static void do_mail_unclear(dbref player, UTF8 *msglist) { do_mail_flags(player, msglist, M_CLEARED, true); } // Change a message's folder. // static void do_mail_file(dbref player, UTF8 *msglist, UTF8 *folder) { struct mail_selector ms; if (!parse_msglist(msglist, &ms, player)) { return; } int foldernum; if ((foldernum = parse_folder(player, folder)) == -1) { raw_notify(player, M_("MAIL: Invalid folder specification")); return; } int i = 0, j = 0; int origfold = player_folder(player); MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( All(ms) || (Folder(mp) == origfold)) { i++; if (mail_match(mp, ms, i)) { j++; // Clear the folder. // mp->read &= M_FMASK; mp->read |= FolderBit(foldernum); sqlite_wt_update_mail_flags(mp); raw_notify(player, tprintf(M_("MAIL: Msg %d filed in folder %d"), i, foldernum)); } } } if (!j) { // Ran off the end of the list without finding anything. // raw_notify(player, M_("MAIL: You don’t have any matching messages!")); } } // A mail alias can be any combination of upper-case letters, lower-case // letters, and digits. No leading digits. No symbols. No ANSI. Length is // limited to SIZEOF_MALIAS-1. Case is preserved. // UTF8 *MakeCanonicalMailAlias ( const UTF8 *pMailAlias, size_t *pnValidMailAlias, bool *pbValidMailAlias ) { thread_local UTF8 Buffer[SIZEOF_MALIAS]; size_t nLeft = sizeof(Buffer)-1; UTF8 *q = Buffer; const UTF8 *p = pMailAlias; if ( !p || !mux_isalpha(*p)) { *pnValidMailAlias = 0; *pbValidMailAlias = false; return nullptr; } *q++ = *p++; nLeft--; while ( *p && nLeft) { if ( !mux_isalpha(*p) && !mux_isdigit(*p) && *p != '_') { break; } *q++ = *p++; nLeft--; } *q = '\0'; *pnValidMailAlias = q - Buffer; *pbValidMailAlias = true; return Buffer; } #define GMA_NOTFOUND 1 #define GMA_FOUND 2 #define GMA_INVALIDFORM 3 static malias_t *get_malias(dbref player, UTF8 *alias, int *pnResult) { *pnResult = GMA_INVALIDFORM; if (!alias) { return nullptr; } if (alias[0] == '#') { if (ExpMail(player)) { int64_t x = mux_atoi64(alias + 1); if (x < 0 || x >= static_cast(malias.size())) { *pnResult = GMA_NOTFOUND; return nullptr; } *pnResult = GMA_FOUND; return malias[x].get(); } } else if (alias[0] == '*') { size_t nValidMailAlias; bool bValidMailAlias; UTF8 *pValidMailAlias = MakeCanonicalMailAlias( alias + 1, &nValidMailAlias, &bValidMailAlias); if (bValidMailAlias) { for (size_t i = 0; i < malias.size(); i++) { malias_t *m = malias[i].get(); if (m->owner == player || m->owner == GOD || ExpMail(player)) { if (!strcmp(reinterpret_cast(pValidMailAlias), m->name.c_str())) { *pnResult = GMA_FOUND; return m; } } } *pnResult = GMA_NOTFOUND; } } if (*pnResult == GMA_INVALIDFORM) { if (ExpMail(player)) { raw_notify(player, M_("MAIL: Mail aliases must be of the form * or #.")); } else { raw_notify(player, M_("MAIL: Mail aliases must be of the form *.")); } } return nullptr; } static UTF8 *make_namelist(dbref player, UTF8 *arg) { UNUSED_PARAMETER(player); UTF8 *p; UTF8 *oldarg = alloc_lbuf("make_namelist.oldarg"); UTF8 *names = alloc_lbuf("make_namelist.names"); UTF8 *bp = names; mux_strncpy(oldarg, arg, LBUF_SIZE-1); string_token st(oldarg, T(" ")); bool bFirst = true; for (p = st.parse(); p; p = st.parse()) { if (!bFirst) { safe_str(T(", "), names, &bp); } bFirst = false; if ( mux_isdigit(p[0]) || ( p[0] == '!' && mux_isdigit(p[1]))) { UTF8 ch = p[0]; if (ch == '!') { p++; } dbref target = mux_atoi64(p); if ( Good_obj(target) && isPlayer(target)) { if (ch == '!') { safe_chr('!', names, &bp); } safe_str(Moniker(target), names, &bp); } } else { safe_str(p, names, &bp); } } *bp = '\0'; free_lbuf(oldarg); return names; } #define NUM_MAILSTATUSTABLE 7 static struct tag_mailstatusentry { int nMask; const UTF8 *pYes; int nYes; const UTF8 *pNo; int nNo; } aMailStatusTable[NUM_MAILSTATUSTABLE] = { { M_ISREAD, T("Read"), 4, T("Unread"), 6 }, { M_CLEARED, T("Cleared"), 7, 0, 0 }, { M_URGENT, T("Urgent"), 6, 0, 0 }, { M_MASS, T("Mass"), 4, 0, 0 }, { M_FORWARD, T("Fwd"), 3, 0, 0 }, { M_TAG, T("Tagged"), 6, 0, 0 }, { M_SAFE, T("Safe"), 4, 0, 0 } }; static UTF8 *status_string(struct mail *mp) { // Return a longer description of message flags. // UTF8 *tbuf1 = alloc_lbuf("status_string"); UTF8 *p = tbuf1; struct tag_mailstatusentry *mse = aMailStatusTable; for (int i = 0; i < NUM_MAILSTATUSTABLE; i++, mse++) { if (mp->read & mse->nMask) { if (p != tbuf1) *p++ = ' '; memcpy(p, mse->pYes, mse->nYes); p += mse->nYes; } else if (mse->pNo) { if (p != tbuf1) *p++ = ' '; memcpy(p, mse->pNo, mse->nNo); p += mse->nNo; } } *p++ = '\0'; return tbuf1; } static void do_mail_read(dbref player, UTF8 *arg1, UTF8 *arg2) { UTF8 *msglist; int folder = player_folder(player); int original_folder = folder; // Check the argument list, if arg2 is present and valid, then lookup // mail in the arg1 folder rather than the default folder. // if ( nullptr == arg2 || '\0' == arg2[0]) { msglist = arg1; } else { folder = parse_folder(player, arg1); if (-1 == folder) { raw_notify(player, M_("MAIL: No such folder.")); return; } set_player_folder(player, folder); msglist = arg2; } struct mail_selector ms; if (!parse_msglist(msglist, &ms, player)) { return; } UTF8 *status, *names; int i = 0, j = 0; UTF8 *buff = alloc_lbuf("do_mail_read.1"); MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( Folder(mp) == folder && mail_to_player(player, mp)) { i++; if (mail_match(mp, ms, i)) { // Read it. // j++; UTF8 *bp = buff; safe_str(MessageFetch(mp->number), buff, &bp); *bp = '\0'; raw_notify(player, DASH_LINE); status = status_string(mp); names = make_namelist(player, const_cast(utf8(mp->tolist))); UTF8 szFromName[MBUF_SIZE]; trimmed_name(mp->from, szFromName, 16, 16, 0); UTF8 szSubjectBuffer[MBUF_SIZE]; StripTabsAndTruncate(utf8(mp->subject), szSubjectBuffer, MBUF_SIZE-1, 65); raw_notify(player, tprintf(T("%-3d From: %s At: %-25s %s\r\nFldr : %-2d Status: %s\r\nTo : %-65s\r\nSubject: %s"), i, szFromName, utf8(mp->time), (Connected(mp->from) && (!Hidden(mp->from) || See_Hidden(player))) ? " (Conn)" : " ", folder, status, names, szSubjectBuffer)); free_lbuf(names); free_lbuf(status); raw_notify(player, DASH_LINE); raw_notify(player, buff); raw_notify(player, DASH_LINE); if (Unread(mp)) { // Mark message as read. // mp->read |= M_ISREAD; sqlite_wt_update_mail_flags(mp); } } } } free_lbuf(buff); // If the folder was changed, restore the original folder setting // if (folder != original_folder) { set_player_folder(player, original_folder); } if (!j) { // Ran off the end of the list without finding anything. // raw_notify(player, M_("MAIL: You don’t have that many matching messages!")); } } static void do_mail_next(dbref player) { int folder = player_folder(player); int i = 0; MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( Folder(mp) == folder && mail_to_player(player, mp)) { i++; if (Unread(mp)) { // Display the message using the same format as do_mail_read. // UTF8 *buff = alloc_lbuf("do_mail_next.1"); UTF8 *bp = buff; safe_str(MessageFetch(mp->number), buff, &bp); *bp = '\0'; raw_notify(player, DASH_LINE); UTF8 *status = status_string(mp); UTF8 *names = make_namelist(player, const_cast(utf8(mp->tolist))); UTF8 szFromName[MBUF_SIZE]; trimmed_name(mp->from, szFromName, 16, 16, 0); UTF8 szSubjectBuffer[MBUF_SIZE]; StripTabsAndTruncate(utf8(mp->subject), szSubjectBuffer, MBUF_SIZE-1, 65); raw_notify(player, tprintf(T("%-3d From: %s At: %-25s %s\r\nFldr : %-2d Status: %s\r\nTo : %-65s\r\nSubject: %s"), i, szFromName, utf8(mp->time), (Connected(mp->from) && (!Hidden(mp->from) || See_Hidden(player))) ? " (Conn)" : " ", folder, status, names, szSubjectBuffer)); free_lbuf(names); free_lbuf(status); raw_notify(player, DASH_LINE); raw_notify(player, buff); raw_notify(player, DASH_LINE); free_lbuf(buff); // Mark message as read. // mp->read |= M_ISREAD; sqlite_wt_update_mail_flags(mp); return; } } } raw_notify(player, M_("MAIL: You have no unread messages in that folder.")); } static UTF8 *status_chars(struct mail *mp) { // Return a short description of message flags. // thread_local UTF8 res[10]; UTF8 *p = res; *p++ = Read(mp) ? '-' : 'N'; *p++ = M_Safe(mp) ? 'S' : '-'; *p++ = Cleared(mp) ? 'C' : '-'; *p++ = Urgent(mp) ? 'U' : '-'; *p++ = Mass(mp) ? 'M' : '-'; *p++ = Forward(mp) ? 'F' : '-'; *p++ = Tagged(mp) ? '+' : '-'; *p = '\0'; return res; } // Folder / review summary line (#1667 Phase 4 B3) — same shape as module // format_mail_list_line: [flags] n (size) From: name(16) Sub:/At: tail. // From uses display-column width via mux_table (not printf codepoints). // static const size_t kMailListFromCols = 16; static const size_t kMailListSubCols = 25; static void format_mail_list_line_sub( UTF8 *line, size_t nLine, const UTF8 *status, int i, size_t nSize, const UTF8 *from, const UTF8 *subject) { size_t pos = 0; pos = mux_table_append_bytes(line, nLine, pos, "["); pos = mux_table_append_bytes(line, nLine, pos, reinterpret_cast(status)); pos = mux_table_append_bytes(line, nLine, pos, "] "); if (pos < nLine) { mux_sprintf(line + pos, nLine - pos, T("%-3d (%4zu) From: "), i, nSize); pos = mux_table_pos_after(line, nLine, pos); } pos = mux_table_append_ljust(line, nLine, pos, from, kMailListFromCols); pos = mux_table_append_bytes(line, nLine, pos, " Sub: "); mux_table_append_trunc(line, nLine, pos, subject, kMailListSubCols); } static void format_mail_list_line_at( UTF8 *line, size_t nLine, const UTF8 *status, int i, size_t nSize, const UTF8 *from, const UTF8 *time_str, const UTF8 *conn_tag) { size_t pos = 0; pos = mux_table_append_bytes(line, nLine, pos, "["); pos = mux_table_append_bytes(line, nLine, pos, reinterpret_cast(status)); pos = mux_table_append_bytes(line, nLine, pos, "] "); if (pos < nLine) { mux_sprintf(line + pos, nLine - pos, T("%-3d (%4zu) From: "), i, nSize); pos = mux_table_pos_after(line, nLine, pos); } pos = mux_table_append_ljust(line, nLine, pos, from, kMailListFromCols); pos = mux_table_append_bytes(line, nLine, pos, " At: "); pos = mux_table_append_bytes(line, nLine, pos, reinterpret_cast( (nullptr != time_str) ? time_str : T(""))); pos = mux_table_append_bytes(line, nLine, pos, " "); pos = mux_table_append_bytes(line, nLine, pos, reinterpret_cast( (nullptr != conn_tag) ? conn_tag : T(""))); } // Returns true if mp was sent by the current incarnation of player. // Guards against recycled dbref: if the mail predates the player's // creation, a previous player held this dbref. // bool mail_from_player(dbref player, struct mail *mp) { if (mp->from != player) { return false; } const UTF8 *pCreated = atr_get_raw(player, A_CREATED); if (nullptr == pCreated) { return false; } CLinearTimeAbsolute ltaCreated, ltaMail; if ( ltaCreated.SetString(pCreated) && ltaMail.SetString(utf8(mp->time))) { return ltaCreated <= ltaMail; } return false; } // Returns true if mp was received by the current incarnation of player. // Guards against recycled dbref: if the mail predates the player's // creation, a previous player held this dbref. // bool mail_to_player(dbref player, struct mail *mp) { if (mp->to != player) { return false; } const UTF8 *pCreated = atr_get_raw(player, A_CREATED); if (nullptr == pCreated) { return false; } CLinearTimeAbsolute ltaCreated, ltaMail; if ( ltaCreated.SetString(pCreated) && ltaMail.SetString(utf8(mp->time))) { return ltaCreated <= ltaMail; } return false; } static void do_mail_review_all(dbref player, UTF8 *msglist) { struct mail *mp; struct mail_selector ms; int i = 0, j = 0; UTF8 szSubjectBuffer[MBUF_SIZE]; UTF8 szFromName[MBUF_SIZE]; if ( !msglist || !*msglist) { // Summary mode: list all sent messages grouped by recipient. // for (auto &kv : mail_storage) { dbref target = kv.first; bool bHeader = false; MailList ml(target); for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (mail_from_player(player, mp)) { i++; if (!bHeader) { trimmed_name(target, szFromName, 25, 25, 0); raw_notify(player, tprintf(T(MAIL_LINE), szFromName)); bHeader = true; } trimmed_name(mp->from, szFromName, 16, 16, 0); size_t nSize = MessageFetchSize(mp->number); UTF8 line[LBUF_SIZE]; format_mail_list_line_sub(line, sizeof(line), status_chars(mp), i, nSize, szFromName, utf8(mp->subject)); raw_notify(player, line); } } } if (0 == i) { raw_notify(player, M_("MAIL: You have no matching messages.")); } else { raw_notify(player, DASH_LINE); } } else { // Detail mode: show full messages matching msglist. // if (!parse_msglist(msglist, &ms, player)) { return; } for (auto &kv : mail_storage) { dbref target = kv.first; MailList ml(target); for (mp = ml.FirstItem(); !ml.IsEnd() && !alarm_clock.alarmed; mp = ml.NextItem()) { if (mail_from_player(player, mp)) { i++; if (mail_match(mp, ms, i)) { j++; UTF8 *status = status_string(mp); const UTF8 *str = MessageFetch(mp->number); trimmed_name(mp->from, szFromName, 16, 16, 0); StripTabsAndTruncate(utf8(mp->subject), szSubjectBuffer, MBUF_SIZE-1, 65); raw_notify(player, DASH_LINE); raw_notify(player, tprintf(T("%-3d From: %s At: %-25s %s\r\nFldr : %-2d Status: %s\r\nSubject: %s"), i, szFromName, utf8(mp->time), (Connected(mp->from) && (!Hidden(mp->from) || See_Hidden(player))) ? " (Conn)" : " ", 0, status, szSubjectBuffer)); free_lbuf(status); raw_notify(player, DASH_LINE); raw_notify(player, str); raw_notify(player, DASH_LINE); } } } } if (!j) { raw_notify(player, M_("MAIL: You don’t have that many matching messages!")); } } } static void do_mail_review(dbref player, UTF8 *name, UTF8 *msglist) { if ( name && '\0' != name[0] && 0 == mux_stricmp(name, T("all"))) { do_mail_review_all(player, msglist); return; } dbref target = lookup_player(player, name, true); if (target == NOTHING) { raw_notify(player, M_("MAIL: No such player.")); return; } struct mail *mp; struct mail_selector ms; int i = 0, j = 0; UTF8 szSubjectBuffer[MBUF_SIZE]; UTF8 szFromName[MBUF_SIZE]; if ( !msglist || !*msglist) { trimmed_name(target, szFromName, 25, 25, 0); raw_notify(player, tprintf(T(MAIL_LINE), szFromName)); MailList ml(target); for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (mail_from_player(player, mp)) { i++; trimmed_name(mp->from, szFromName, 16, 16, 0); size_t nSize = MessageFetchSize(mp->number); UTF8 line[LBUF_SIZE]; format_mail_list_line_sub(line, sizeof(line), status_chars(mp), i, nSize, szFromName, utf8(mp->subject)); raw_notify(player, line); } } raw_notify(player, DASH_LINE); } else { if (!parse_msglist(msglist, &ms, target)) { return; } MailList ml(target); for (mp = ml.FirstItem(); !ml.IsEnd() && !alarm_clock.alarmed; mp = ml.NextItem()) { if (mail_from_player(player, mp)) { i++; if (mail_match(mp, ms, i)) { j++; UTF8 *status = status_string(mp); const UTF8 *str = MessageFetch(mp->number); trimmed_name(mp->from, szFromName, 16, 16, 0); StripTabsAndTruncate(utf8(mp->subject), szSubjectBuffer, MBUF_SIZE-1, 65); raw_notify(player, DASH_LINE); raw_notify(player, tprintf(T("%-3d From: %s At: %-25s %s\r\nFldr : %-2d Status: %s\r\nSubject: %s"), i, szFromName, utf8(mp->time), (Connected(mp->from) && (!Hidden(mp->from) || See_Hidden(player))) ? " (Conn)" : " ", 0, status, szSubjectBuffer)); free_lbuf(status); raw_notify(player, DASH_LINE); raw_notify(player, str); raw_notify(player, DASH_LINE); } } } if (!j) { // Ran off the end of the list without finding anything. // raw_notify(player, M_("MAIL: You don’t have that many matching messages!")); } } } static UTF8 *mail_list_time(const UTF8 *the_time) { const UTF8 *p = the_time; UTF8 *new0 = alloc_lbuf("mail_list_time"); UTF8 *q = new0; if (!p || !*p) { *new0 = '\0'; return new0; } // Format of the_time is: day mon dd hh:mm:ss yyyy // Chop out :ss // int i; for (i = 0; i < 16; i++) { if (*p) { *q++ = *p++; } } for (i = 0; i < 3; i++) { if (*p) { p++; } } for (i = 0; i < 5; i++) { if (*p) { *q++ = *p++; } } *q = '\0'; return new0; } static void do_mail_list(dbref player, UTF8 *arg1, UTF8 *arg2, bool sub) { UTF8 *msglist; int folder = player_folder(player); int original_folder = folder; // Check the argument list, if arg2 is present and valid, then lookup // mail in the arg1 folder rather than the default folder. // if ( nullptr == arg2 || '\0' == arg2[0]) { msglist = arg1; } else { folder = parse_folder(player, arg1); if (-1 == folder) { raw_notify(player, M_("MAIL: No such folder.")); return; } set_player_folder(player, folder); msglist = arg2; if ( nullptr != msglist && '*' == msglist[0]) { msglist[0] = '\0'; } sub = true; } struct mail_selector ms; if (!parse_msglist(msglist, &ms, player)) { return; } int i = 0; raw_notify(player, tprintf(T(FOLDER_LINE), folder)); MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( Folder(mp) == folder && mail_to_player(player, mp)) { i++; if (mail_match(mp, ms, i)) { size_t nSize = MessageFetchSize(mp->number); UTF8 szFromName[MBUF_SIZE]; trimmed_name(mp->from, szFromName, 16, 16, 0); UTF8 line[LBUF_SIZE]; if (sub) { format_mail_list_line_sub(line, sizeof(line), status_chars(mp), i, nSize, szFromName, utf8(mp->subject)); } else { UTF8 *time = mail_list_time(utf8(mp->time)); format_mail_list_line_at(line, sizeof(line), status_chars(mp), i, nSize, szFromName, time, (Connected(mp->from) && (!Hidden(mp->from) || See_Hidden(player))) ? T("Conn") : T(" ")); free_lbuf(time); } raw_notify(player, line); } } } raw_notify(player, DASH_LINE); if (folder != original_folder) { set_player_folder(player, original_folder); } } void do_mail_purge(dbref player) { // Go through player's mail, and remove anything marked cleared. // MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (Cleared(mp)) { ml.RemoveItem(); } } raw_notify(player, M_("MAIL: Mailbox purged.")); } static UTF8 *make_numlist(dbref player, UTF8 *arg, bool bBlind) { UTF8 *tail, spot; malias_t *m; dbref target; int nRecip = 0; dbref aRecip[(LBUF_SIZE+1)/2]; UTF8 *head = arg; while ( head && *head) { while (*head == ' ') { head++; } tail = head; while ( *tail && *tail != ' ') { if (*tail == '"') { head++; tail++; while ( *tail && *tail != '"') { tail++; } } if (*tail) { tail++; } } // #1197: guard like the module path — a lone '"' leaves tail == head // and must not be decremented (would underflow before *tail write). // if (tail > head) { tail--; if (*tail != '"') { tail++; } } spot = *tail; *tail = '\0'; if (*head == '*') { int nResult; m = get_malias(player, head, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ does not exist."), head)); return nullptr; } else if (nResult == GMA_INVALIDFORM) { raw_notify(player, tprintf(M_("MAIL: ‘%s’ is a badly-formed alias."), head)); return nullptr; } for (size_t i = 0; i < m->list.size() && nRecip < static_cast(sizeof(aRecip)/sizeof(aRecip[0])); i++) { aRecip[nRecip++] = m->list[i]; } } else { target = lookup_player(player, head, true); if (Good_obj(target)) { // Bound the write like the alias-copy loop above: aRecip[] is // sized (LBUF_SIZE+1)/2, and a single clamped malias can fill // it, so a valid recipient past the cap is dropped rather than // overflowing the stack array. // if (nRecip < static_cast(sizeof(aRecip)/sizeof(aRecip[0]))) { aRecip[nRecip++] = target; } } else { raw_notify(player, tprintf(M_("MAIL: ‘%s’ does not exist."), head)); return nullptr; } } // Get the next recip. // *tail = spot; head = tail; if (*head == '"') { head++; } } if (nRecip <= 0) { raw_notify(player, M_("MAIL: No players specified.")); return nullptr; } else { ITL itl; UTF8 *numbuf, *numbp; numbp = numbuf = alloc_lbuf("mail.make_numlist"); ItemToList_Init(&itl, numbuf, &numbp, bBlind ? '!' : '\0'); int i; for (i = 0; i < nRecip; i++) { if (aRecip[i] != NOTHING) { for (int j = i + 1; j < nRecip; j++) { if (aRecip[i] == aRecip[j]) { aRecip[j] = NOTHING; } } if (Good_obj(aRecip[i])) { ItemToList_AddInteger(&itl, aRecip[i]); } } } ItemToList_Final(&itl); return numbuf; } } static void do_expmail_start(dbref player, UTF8 *arg, UTF8 *subject) { if (!arg || !*arg) { raw_notify(player, M_("MAIL: I do not know whom you want to mail.")); return; } if (!subject || !*subject) { raw_notify(player, M_("MAIL: No subject.")); return; } if (Flags2(player) & PLAYER_MAILS) { raw_notify(player, M_("MAIL: Mail message already in progress.")); return; } if ( !Wizard(player) && ThrottleMail(player)) { raw_notify(player, M_("MAIL: Too much @mail sent recently.")); return; } UTF8 *tolist = make_numlist(player, arg, false); if (!tolist) { return; } atr_add_raw(player, A_MAILTO, tolist); atr_add_raw(player, A_MAILSUB, subject); atr_add_raw(player, A_MAILFLAGS, T("0")); atr_clr(player, A_MAILMSG); Flags2(player) |= PLAYER_MAILS; UTF8 *names = make_namelist(player, tolist); raw_notify(player, tprintf(M_("MAIL: You are sending mail to ‘%s’."), names)); free_lbuf(names); free_lbuf(tolist); } static void do_mail_fwd(dbref player, UTF8 *msg, UTF8 *tolist) { if (Flags2(player) & PLAYER_MAILS) { raw_notify(player, M_("MAIL: Mail message already in progress.")); return; } if (!msg || !*msg) { raw_notify(player, M_("MAIL: No message list.")); return; } if (!tolist || !*tolist) { raw_notify(player, M_("MAIL: To whom should I forward?")); return; } if ( !Wizard(player) && ThrottleMail(player)) { raw_notify(player, M_("MAIL: Too much @mail sent recently.")); return; } int64_t num = mux_atoi64(msg); if (!num) { raw_notify(player, M_("MAIL: I don’t understand that message number.")); return; } struct mail *mp = mail_fetch(player, num); if (!mp) { raw_notify(player, M_("MAIL: You can’t forward non-existent messages.")); return; } do_expmail_start(player, tolist, tprintf(M_("%s (fwd from %s)"), utf8(mp->subject), Moniker(mp->from))); atr_add_raw(player, A_MAILMSG, MessageFetch(mp->number)); const UTF8 *pValue = atr_get_raw(player, A_MAILFLAGS); int iFlag = M_FORWARD; if (pValue) { iFlag |= mux_atoi64(pValue); } atr_add_raw(player, A_MAILFLAGS, mux_ltoa_t(iFlag)); } static void do_mail_reply(dbref player, UTF8 *msg, bool all, int key) { if (Flags2(player) & PLAYER_MAILS) { raw_notify(player, M_("MAIL: Mail message already in progress.")); return; } if (!msg || !*msg) { raw_notify(player, M_("MAIL: No message list.")); return; } if ( !Wizard(player) && ThrottleMail(player)) { raw_notify(player, M_("MAIL: Too much @mail sent recently.")); return; } int64_t num = mux_atoi64(msg); if (!num) { raw_notify(player, M_("MAIL: I don’t understand that message number.")); return; } struct mail *mp = mail_fetch(player, num); if (!mp) { raw_notify(player, M_("MAIL: You can’t reply to non-existent messages.")); return; } if (!mail_from_player(mp->from, mp)) { raw_notify(player, M_("MAIL: The original sender no longer exists.")); return; } UTF8 *tolist = alloc_lbuf("do_mail_reply.tolist"); UTF8 *bp = tolist; if (all) { UTF8 *names = alloc_lbuf("do_mail_reply.names"); UTF8 *oldlist = alloc_lbuf("do_mail_reply.oldlist"); bp = names; *bp = '\0'; mux_strncpy(oldlist, utf8(mp->tolist), LBUF_SIZE-1); string_token st(oldlist, T(" ")); UTF8 *p; for (p = st.parse(); p; p = st.parse()) { if (mux_atoi64(p) != mp->from) { safe_chr('#', names, &bp); safe_str(p, names, &bp); safe_chr(' ', names, &bp); } } free_lbuf(oldlist); safe_chr('#', names, &bp); safe_ltoa(mp->from, names, &bp); *bp = '\0'; mux_strncpy(tolist, names, LBUF_SIZE-1); free_lbuf(names); } else { safe_chr('#', tolist, &bp); safe_ltoa(mp->from, tolist, &bp); *bp = '\0'; } const UTF8 *pSubject = utf8(mp->subject); const UTF8 *pMessage = MessageFetch(mp->number); const UTF8 *pTime = utf8(mp->time); if (strncmp(reinterpret_cast(pSubject), "Re:", 3)) { do_expmail_start(player, tolist, tprintf(M_("Re: %s"), pSubject)); } else { do_expmail_start(player, tolist, tprintf(T("%s"), pSubject)); } if (key & MAIL_QUOTE) { const UTF8 *pFromName = Moniker(mp->from); UTF8 *pMessageBody = tprintf(M_("On %s, %s wrote:\r\n\r\n%s\r\n\r\n********** End of included message from %s\r\n"), pTime, pFromName, pMessage, pFromName); atr_add_raw(player, A_MAILMSG, pMessageBody); } // The following combination of atr_get_raw() with atr_add_raw() is OK // because we are not passing a pointer to atr_add_raw() that came // directly from atr_get_raw(). // const UTF8 *pValue = atr_get_raw(player, A_MAILFLAGS); int iFlag = M_REPLY; if (pValue) { iFlag |= mux_atoi64(pValue); } atr_add_raw(player, A_MAILFLAGS, mux_ltoa_t(iFlag)); free_lbuf(tolist); } /*-------------------------------------------------------------------------* * Admin mail functions * * do_mail_nuke - clear & purge mail for a player, or all mail in db. * do_mail_stat - stats on mail for a player, or for all db. * do_mail_debug - fix mail with a sledgehammer *-------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------* * Basic mail functions *-------------------------------------------------------------------------*/ // #1191: Softcode mail_* helpers use engine maps; re-sync from SQLite when // the module store is ahead. // static void ensure_mail_softcode_sync(void) { if (nullptr == mudstate.pIMailControl) { return; } // Re-entrancy guard, mirroring the comsys side: the loader runs engine // paths that can call back into us before s_seen_rev is assigned. // if (mudstate.bSQLiteLoading) { return; } static int s_seen_rev = -1; int rev = 0; MUX_RESULT mr = mudstate.pIMailControl->GetRevision(&rev); if (MUX_FAILED(mr)) { return; } if (rev == s_seen_rev) { return; } (void)sqlite_load_mail(); s_seen_rev = rev; } struct mail *mail_fetch(dbref player, int num) { ensure_mail_softcode_sync(); int i = 0; MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( Folder(mp) == player_folder(player) && mail_to_player(player, mp)) { i++; if (i == num) { return mp; } } } return nullptr; } const UTF8 *mail_fetch_message(dbref player, int num) { struct mail *mp = mail_fetch(player, num); if (mp) { return MessageFetch(mp->number); } return nullptr; } int mail_fetch_from(dbref player, int num) { struct mail *mp = mail_fetch(player, num); if (mp) { return mp->from; } return NOTHING; } // Returns count of read, unread, and cleared messages as rcount, ucount, ccount. // void count_mail(dbref player, int folder, int *rcount, int *ucount, int *ccount) { if (nullptr != mudstate.pIMailControl) { MUX_RESULT mr = mudstate.pIMailControl->CountMail(player, folder, rcount, ucount, ccount); if (MUX_SUCCEEDED(mr)) { return; } } ensure_mail_softcode_sync(); int rc = 0; int uc = 0; int cc = 0; MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( Folder(mp) == folder && mail_to_player(player, mp)) { if (Read(mp)) { rc++; } else { uc++; } if (Cleared(mp)) { cc++; } } } *rcount = rc; *ucount = uc; *ccount = cc; } static void urgent_mail(dbref player, int folder, int *ucount) { int uc = 0; MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( Folder(mp) == folder && mail_to_player(player, mp)) { if (Unread(mp) && Urgent(mp)) { uc++; } } } *ucount = uc; } static void mail_return(dbref player, dbref target) { dbref aowner; int aflags; UTF8 *str = atr_pget(target, A_MFAIL, &aowner, &aflags); if (*str) { UTF8 *str2, *bp; str2 = bp = alloc_lbuf("mail_return"); mux_exec(str, LBUF_SIZE-1, str2, &bp, target, player, player, AttrTrace(aflags, EV_FCHECK|EV_EVAL|EV_TOP|EV_NO_LOCATION), nullptr, 0); *bp = '\0'; if (*str2) { CLinearTimeAbsolute ltaNow; ltaNow.GetLocal(); FIELDEDTIME ft; ltaNow.ReturnFields(&ft); raw_notify(player, tprintf(M_("MAIL: Reject message from %s: %s"), Moniker(target), str2)); raw_notify(target, tprintf(M_("[%d:%02d] MAIL: Reject message sent to %s."), ft.iHour, ft.iMinute, Moniker(player))); } free_lbuf(str2); } else { raw_notify(player, tprintf(M_("Sorry, %s is not accepting mail."), Moniker(target))); } free_lbuf(str); } static bool mail_check(dbref player, dbref target) { if (!could_doit(player, target, A_LMAIL)) { mail_return(player, target); } else if (!could_doit(target, player, A_LMAIL)) { if (Wizard(player)) { raw_notify(player, tprintf(M_("Warning: %s can’t return your mail."), Moniker(target))); return true; } else { raw_notify(player, tprintf(M_("Sorry, %s can’t return your mail."), Moniker(target))); return false; } } else { return true; } return false; } static void send_mail ( dbref player, dbref target, const UTF8 *tolist, const UTF8 *subject, int number, mail_flag flags, bool silent ) { if (!isPlayer(target)) { raw_notify(player, M_("MAIL: You cannot send mail to non-existent people.")); return; } if (!mail_check(player, target)) { return; } CLinearTimeAbsolute ltaNow; ltaNow.GetLocal(); const UTF8 *pTimeStr = ltaNow.ReturnDateString(0); // Initialize the appropriate fields. // auto& lst = mail_storage[target]; lst.emplace_back(); mail& newm = lst.back(); newm.to = target; newm.sqlite_id = -1; // Sender attribution policy for @mail/quick from objects: // - If the sender is a player, credit the player. // - If the sender is an object owned by a wizard, credit the // object itself (wizards can run trusted delivery bots). // - Otherwise credit the object's owner (so a non-wizard player // can't spoof mail origin by using an intermediate object). // if (isPlayer(player)) { newm.from = player; } else { dbref mailbag = Owner(player); if (Wizard(mailbag)) { newm.from = player; } else { newm.from = mailbag; } } if ( !tolist || tolist[0] == '\0') { newm.tolist = "*HIDDEN*"; } else { newm.tolist.assign(reinterpret_cast(tolist)); } newm.number = number; MessageReferenceInc(number); newm.time.assign(reinterpret_cast(pTimeStr)); newm.subject.assign(reinterpret_cast(subject)); // Send to folder 0 // newm.read = flags & M_FMASK; // Reject if the target's mailbox is full. // if ( 0 < mudconf.mail_max_per_player && !No_Mail_Expire(target)) { int total = 0; MailList ml_count(target); struct mail *mp_count; for (mp_count = ml_count.FirstItem(); !ml_count.IsEnd(); mp_count = ml_count.NextItem()) { total++; } // total already includes the message we emplaced above, so the limit // is reached at max+1 (existing == max). Report the pre-existing // count (total-1) to match the prior behavior. // if (total > mudconf.mail_max_per_player) { raw_notify(player, tprintf(MN_("MAIL: %s’s mailbox is full (%d message).", "MAIL: %s’s mailbox is full (%d messages).", total - 1), Moniker(target), total - 1)); MessageReferenceDec(number); lst.pop_back(); // undo the emplace return; } } // The message is committed to the target's in-memory list; write it // through to SQLite so it is durable and so later read/delete mutations // (which key off sqlite_id) persist. Mirrors the pre-refactor insert // that the STL migration dropped. // sqlite_wt_insert_mail(&newm); // Notify people. // if (!silent) { raw_notify(player, tprintf(M_("MAIL: You sent your message to %s."), Moniker(target))); } raw_notify(target, tprintf(M_("MAIL: You have a new message from %s. Subject: %s"), Moniker(player), subject)); did_it(player, target, A_MAIL, nullptr, 0, nullptr, A_AMAIL, 0, nullptr, NOTHING); } static void do_mail_nuke(dbref player) { if (!God(player)) { raw_notify(player, M_("The postal service issues a warrant for your arrest.")); return; } // Walk the list. // dbref thing; DO_WHOLE_DB(thing) { MailList ml(thing); ml.RemoveAll(); } log_printf(T("** MAIL PURGE ** done by %s(#%d)." ENDLINE), PureName(player), player); raw_notify(player, M_("You annihilate the post office. All messages cleared.")); } // Purge a destroyed player's mail presence. Called from destroy_player(). // // 1. Remove all received mail. // 2. Orphan sent mail so a recycled dbref cannot reply to the old player. // 3. Clean up mail aliases. // void mail_destroy_player(dbref victim) { if (nullptr != mudstate.pIMailControl) { MUX_RESULT mr = mudstate.pIMailControl->DestroyPlayerMail(victim); if (MUX_SUCCEEDED(mr)) { return; } } // Step 1: Purge received mail. // MailList ml(victim); ml.RemoveAll(); // Step 2: Orphan sent mail in every other player's mailbox. // for (auto &kv : mail_storage) { MailList ml2(kv.first); struct mail *mp; for (mp = ml2.FirstItem(); !ml2.IsEnd(); mp = ml2.NextItem()) { if (mp->from == victim) { mp->from = NOTHING; } } } // Step 3: Clean up mail aliases. // malias_cleanup(victim); } #ifdef SELFCHECK void finish_mail() { dbref thing; DO_WHOLE_DB(thing) { MailList ml(thing); ml.RemoveAll(); } mail_list.clear(); } #endif static void do_mail_debug(dbref player, UTF8 *action, UTF8 *victim) { if (!ExpMail(player)) { raw_notify(player, M_("Go get some bugspray.")); return; } dbref thing; if (string_prefix(T("clear"), action)) { dbref target = lookup_player(player, victim, true); if (target == NOTHING) { init_match(player, victim, NOTYPE); match_absolute(); target = match_result(); } if (target == NOTHING) { raw_notify(player, tprintf(M_("%s: no such player."), victim)); return; } if (Wizard(target)) { raw_notify(player, tprintf(M_("Let %s clear their own @mail."), Moniker(target))); return; } do_mail_clear(target, nullptr); do_mail_purge(target); raw_notify(player, tprintf(M_("Mail cleared for %s(#%d)."), Moniker(target), target)); return; } else if (string_prefix(T("sanity"), action)) { std::vector ai(mudstate.mail_db_top, 0); DO_WHOLE_DB(thing) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { bool bGoodReference; if (0 <= mp->number && mp->number < mudstate.mail_db_top) { ai[mp->number]++; bGoodReference = true; } else { bGoodReference = false; } if (!Good_obj(mp->to)) { if (bGoodReference) { raw_notify(player, tprintf(M_("Bad object #%d has mail."), mp->to)); } else { raw_notify(player, tprintf(M_("Bad object #%d has mail which refers to a non-existent mailbag item."), mp->to)); } } else if (!isPlayer(mp->to)) { if (bGoodReference) { raw_notify(player, tprintf(M_("%s(#%d) has mail, but is not a player."), Moniker(mp->to), mp->to)); } else { raw_notify(player, tprintf(M_("%s(#%d) is not a player, but has mail which refers to a non-existent mailbag item."), Moniker(mp->to), mp->to)); } } else if (!bGoodReference) { raw_notify(player, tprintf(M_("%s(#%d) has mail which refers to a non-existent mailbag item."), Moniker(mp->to), mp->to)); } } } // Check ref counts. // { int i; int nCountHigher = 0; int nCountLower = 0; for (i = 0; i < mudstate.mail_db_top; i++) { if (mail_list[i].m_nRefs < ai[i]) { nCountLower++; } else if (mail_list[i].m_nRefs > ai[i]) { nCountHigher++; } } if (nCountLower) { raw_notify(player, M_("Some mailbag items are referred to more often than the mailbag item indicates.")); } if (nCountHigher) { raw_notify(player, M_("Some mailbag items are referred to less often than the mailbag item indicates.")); } } raw_notify(player, M_("Mail sanity check completed.")); } else if (string_prefix(T("fix"), action)) { // First, we should fixup the reference counts. // { raw_notify(player, M_("Re-counting mailbag reference counts.")); std::vector ai(mudstate.mail_db_top, 0); DO_WHOLE_DB(thing) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( 0 <= mp->number && mp->number < mudstate.mail_db_top) { ai[mp->number]++; } else { mp->number = NOTHING; } } } int i; int nCountWrong = 0; for (i = 0; i < mudstate.mail_db_top; i++) { if (mail_list[i].m_nRefs != ai[i]) { mail_list[i].m_nRefs = ai[i]; nCountWrong++; } } if (nCountWrong) { raw_notify(player, M_("Some reference counts were wrong [FIXED].")); } } raw_notify(player, M_("Removing @mail that is associated with non-players.")); // Now, remove all mail to non-good or non-players, or mail that // points to non-existent mailbag items. // DO_WHOLE_DB(thing) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if ( !Good_obj(mp->to) || !isPlayer(mp->to) || NOTHING == mp->number) { // Delete this item. // raw_notify(player, tprintf(M_("Fixing mail for #%d."), mp->to)); ml.RemoveItem(); } } } raw_notify(player, M_("Mail sanity fix completed.")); } else { raw_notify(player, M_("That is not a debugging option.")); return; } } static void do_mail_stats(dbref player, UTF8 *name, int full) { dbref target, thing; int fc, fr, fu, tc, tr, tu, count; size_t cchars = 0; size_t fchars = 0; size_t tchars = 0; fc = fr = fu = tc = tr = tu = count = 0; // Find player. // if ( !name || *name == '\0') { if (Wizard(player)) { target = AMBIGUOUS; } else { target = player; } } else if (*name == NUMBER_TOKEN) { target = mux_atoi64(&name[1]); if (!Good_obj(target) || !isPlayer(target)) { target = NOTHING; } } else if (!mux_stricmp(name, T("me"))) { target = player; } else { target = lookup_player(player, name, true); } if (target == NOTHING) { init_match(player, name, NOTYPE); match_absolute(); target = match_result(); } if (target == NOTHING) { raw_notify(player, tprintf(M_("%s: No such player."), name)); return; } if (!ExpMail(player) && (target != player)) { raw_notify(player, M_("The post office protects privacy!")); return; } // This comand is computationally expensive. // if (!payfor(player, mudconf.searchcost)) { raw_notify(player, tprintf(M_("Finding mail stats costs %d %s."), mudconf.searchcost, (mudconf.searchcost == 1) ? mudconf.one_coin : mudconf.many_coins)); return; } if (AMBIGUOUS == target) { // Stats for all. // if (full == 0) { DO_WHOLE_DB(thing) { MailList ml(thing); for ((void)ml.FirstItem(); !ml.IsEnd(); (void)ml.NextItem()) { count++; } } // MN_() rather than T() (#1631): these were never marked, so mail // stats were untranslatable; and "There are 1 messages" was the // #1622 shape -- English morphology decided in C. One change // fixes both. // raw_notify(player, tprintf(MN_("There is %d message in the mail spool.", "There are %d messages in the mail spool.", count), count)); return; } else if (full == 1) { DO_WHOLE_DB(thing) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (Cleared(mp)) { fc++; } else if (Read(mp)) { fr++; } else { fu++; } } } raw_notify(player, tprintf(MN_("MAIL: There is %d msg in the mail spool, %d unread, %d cleared.", "MAIL: There are %d msgs in the mail spool, %d unread, %d cleared.", fc + fr + fu), fc + fr + fu, fu, fc)); return; } else { DO_WHOLE_DB(thing) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { // These were MessageFetchSize() + 1, counting a NUL that // MessageFetchSize does not report -- it returns // std::string::size(). That made @mail/fstats disagree // with @mail/list, mailinfo(size) and the mail module // about how big the same message is, by one byte per // message (#1639). // if (Cleared(mp)) { fc++; cchars += MessageFetchSize(mp->number); } else if (Read(mp)) { fr++; fchars += MessageFetchSize(mp->number); } else { fu++; tchars += MessageFetchSize(mp->number); } } } raw_notify(player, tprintf(MN_("MAIL: There is %d old msg in the mail spool, totalling %d characters.", "MAIL: There are %d old msgs in the mail spool, totalling %d characters.", fr), fr, fchars)); raw_notify(player, tprintf(MN_("MAIL: There is %d new msg in the mail spool, totalling %d characters.", "MAIL: There are %d new msgs in the mail spool, totalling %d characters.", fu), fu, tchars)); raw_notify(player, tprintf(MN_("MAIL: There is %d cleared msg in the mail spool, totalling %d characters.", "MAIL: There are %d cleared msgs in the mail spool, totalling %d characters.", fc), fc, cchars)); return; } } // individual stats // if (full == 0) { // Just count the number of messages. // DO_WHOLE_DB(thing) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (mp->from == target) { fr++; } if (mp->to == target) { tr++; } } } raw_notify(player, tprintf(MN_("%s sent %d message.", "%s sent %d messages.", fr), Moniker(target), fr)); raw_notify(player, tprintf(MN_("%s has %d message.", "%s has %d messages.", tr), Moniker(target), tr)); return; } // More detailed message count. // UTF8 last[50]; DO_WHOLE_DB(thing) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (mp->from == target) { if (Cleared(mp)) { fc++; } else if (Read(mp)) { fr++; } else { fu++; } // Same phantom byte as the whole-DB branch above (#1639). // The report named only the three sites there; these two, // reached by @mail/fstats , had it as well. // if (full == 2) { fchars += MessageFetchSize(mp->number); } } if (mp->to == target) { if (!tr && !tu) { mux_strncpy(last, utf8(mp->time), sizeof(last)-1); } if (Cleared(mp)) { tc++; } else if (Read(mp)) { tr++; } else { tu++; } if (full == 2) { tchars += MessageFetchSize(mp->number); } } } } raw_notify(player, tprintf(M_("Mail statistics for %s:"), Moniker(target))); if (full == 1) { raw_notify(player, tprintf(MN_("%d message sent, %d unread, %d cleared.", "%d messages sent, %d unread, %d cleared.", fr + fu + fc), fc + fr + fu, fu, fc)); raw_notify(player, tprintf(MN_("%d message received, %d unread, %d cleared.", "%d messages received, %d unread, %d cleared.", tr + tu + tc), tc + tr + tu, tu, tc)); } else { raw_notify(player, tprintf(MN_("%d message sent, %d unread, %d cleared, totalling %d characters.", "%d messages sent, %d unread, %d cleared, totalling %d characters.", fr + fu + fc), fc + fr + fu, fu, fc, fchars)); raw_notify(player, tprintf(MN_("%d message received, %d unread, %d cleared, totalling %d characters.", "%d messages received, %d unread, %d cleared, totalling %d characters.", tr + tu + tc), tc + tr + tu, tu, tc, tchars)); } if (tc + tr + tu > 0) { raw_notify(player, tprintf(M_("Last is dated %s"), last)); } } /*-------------------------------------------------------------------------* * Main mail routine for @mail w/o a switch *-------------------------------------------------------------------------*/ static void do_mail_stub(dbref player, UTF8 *arg1, UTF8 *arg2) { if (!arg1 || !*arg1) { if (arg2 && *arg2) { raw_notify(player, M_("MAIL: Invalid mail command.")); return; } // Just the "@mail" command. // do_mail_list(player, arg1, nullptr, true); return; } // purge a player's mailbox // if (!mux_stricmp(arg1, T("purge"))) { do_mail_purge(player); return; } // clear message // if (!mux_stricmp(arg1, T("clear"))) { do_mail_clear(player, arg2); return; } if (!mux_stricmp(arg1, T("unclear"))) { do_mail_unclear(player, arg2); return; } if (arg2 && *arg2) { // Sending mail // do_expmail_start(player, arg1, arg2); return; } else { // Must be reading or listing mail - no arg2 // if ( mux_isdigit(*arg1) && !strchr(reinterpret_cast(arg1), '-')) { do_mail_read(player, arg1, nullptr); } else { do_mail_list(player, arg1, nullptr, true); } return; } } static void malias_write(FILE *fp) { putref(fp, static_cast(malias.size())); for (size_t i = 0; i < malias.size(); i++) { malias_t *m = malias[i].get(); mux_fprintf(fp, T("%d %d\n"), m->owner, static_cast(m->list.size())); mux_fprintf(fp, T("N:%s\n"), m->name.c_str()); mux_fprintf(fp, T("D:%s\n"), m->desc.c_str()); for (size_t j = 0; j < m->list.size(); j++) { putref(fp, m->list[j]); } } } static void save_malias(FILE *fp) { mux_fprintf(fp, T("*** Begin MALIAS ***\n")); malias_write(fp); } int dump_mail(FILE *fp) { dbref thing; int count = 0, i; // Write out version number // mux_fprintf(fp, T("+V6\n")); putref(fp, mudstate.mail_db_top); DO_WHOLE_DB(thing) { if (isPlayer(thing)) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { putref(fp, mp->to); putref(fp, mp->from); putref(fp, mp->number); putstring(fp, utf8(mp->tolist)); putstring(fp, utf8(mp->time)); putstring(fp, utf8(mp->subject)); putref(fp, mp->read); count++; } } } mux_fprintf(fp, T("*** END OF DUMP ***\n")); // Add the db of mail messages // for (i = 0; i < mudstate.mail_db_top; i++) { if (0 < mail_list[i].m_nRefs) { putref(fp, i); putstring(fp, MessageFetch(i)); } } mux_fprintf(fp, T("+++ END OF DUMP +++\n")); save_malias(fp); return count; } static void malias_read(FILE *fp, bool bConvert); static void load_mail_V6(FILE *fp) { int mail_top = getref(fp); mail_db_grow(mail_top + 1); size_t nBuffer; UTF8 *pBuffer; UTF8 nbuf1[200]; UTF8 *p = reinterpret_cast(fgets(reinterpret_cast(nbuf1), sizeof(nbuf1), fp)); while ( nullptr != p && strncmp(reinterpret_cast(nbuf1), "***", 3) != 0) { dbref to = mux_atoi64(nbuf1); auto& lst = mail_storage[ to ]; lst.emplace_back(); mail& m = lst.back(); m.to = to; m.from = getref(fp); m.number = getref(fp); MessageReferenceInc(m.number); pBuffer = reinterpret_cast(getstring_noalloc(fp, true, &nBuffer)); m.tolist.assign(reinterpret_cast(pBuffer), nBuffer); pBuffer = reinterpret_cast(getstring_noalloc(fp, true, &nBuffer)); m.time.assign(reinterpret_cast(pBuffer), nBuffer); pBuffer = reinterpret_cast(getstring_noalloc(fp, true, &nBuffer)); m.subject.assign(reinterpret_cast(pBuffer), nBuffer); m.read = getref(fp); m.sqlite_id = -1; p = reinterpret_cast(fgets(reinterpret_cast(nbuf1), sizeof(nbuf1), fp)); } p = reinterpret_cast(fgets(reinterpret_cast(nbuf1), sizeof(nbuf1), fp)); while ( nullptr != p && strncmp(reinterpret_cast(nbuf1), "+++", 3) != 0) { int64_t number = mux_atoi64(nbuf1); pBuffer = reinterpret_cast(getstring_noalloc(fp, true, &nBuffer)); new_mail_message(pBuffer, number); p = reinterpret_cast(fgets(reinterpret_cast(nbuf1), sizeof(nbuf1), fp)); } p = reinterpret_cast(fgets(reinterpret_cast(nbuf1), sizeof(nbuf1), fp)); if ( nullptr != p && strcmp(reinterpret_cast(nbuf1), "*** Begin MALIAS ***\n") == 0) { malias_read(fp, false); } else { Log.WriteString(T("ERROR: Couldn’t find Begin MALIAS." ENDLINE)); } } static void load_mail_V5(FILE *fp) { int mail_top = getref(fp); mail_db_grow(mail_top + 1); size_t nBufferLatin1; char *pBufferLatin1; size_t nBufferUnicode; UTF8 *pBufferUnicode; char nbuf1[200]; char *p = fgets(nbuf1, sizeof(nbuf1), fp); while ( nullptr != p && strncmp(nbuf1, "***", 3) != 0) { pBufferUnicode = reinterpret_cast(nbuf1); dbref to = mux_atoi64(pBufferUnicode); auto& lst = mail_storage[to]; lst.emplace_back(); mail& m = lst.back(); m.to = to; m.from = getref(fp); m.number = getref(fp); MessageReferenceInc(m.number); pBufferLatin1 = reinterpret_cast(getstring_noalloc(fp, true, &nBufferLatin1)); pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode); m.tolist.assign(reinterpret_cast(pBufferUnicode), nBufferUnicode); pBufferLatin1 = reinterpret_cast(getstring_noalloc(fp, true, &nBufferLatin1)); pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode); m.time.assign(reinterpret_cast(pBufferUnicode), nBufferUnicode); pBufferLatin1 = reinterpret_cast(getstring_noalloc(fp, true, &nBufferLatin1)); pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode); m.subject.assign(reinterpret_cast(pBufferUnicode), nBufferUnicode); m.read = getref(fp); m.sqlite_id = -1; p = fgets(nbuf1, sizeof(nbuf1), fp); } p = fgets(nbuf1, sizeof(nbuf1), fp); while ( nullptr != p && strncmp(nbuf1, "+++", 3) != 0) { pBufferUnicode = reinterpret_cast(nbuf1); int64_t number = mux_atoi64(pBufferUnicode); pBufferLatin1 = reinterpret_cast(getstring_noalloc(fp, true, &nBufferLatin1)); pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode); new_mail_message(pBufferUnicode, number); p = fgets(nbuf1, sizeof(nbuf1), fp); } p = fgets(nbuf1, sizeof(nbuf1), fp); if ( nullptr != p && strcmp(nbuf1, "*** Begin MALIAS ***\n") == 0) { malias_read(fp, true); } else { Log.WriteString(T("ERROR: Couldn’t find Begin MALIAS." ENDLINE)); } } // A mail alias description can be any combination of upper-case letters, // lower-case letters, digits, blanks, and symbols. ANSI is permitted. // Length is limited to SIZEOF_MALIASDESC-1. Visual width is limited to // WIDTHOF_MALIASDESC. Case is preserved. // UTF8 *MakeCanonicalMailAliasDesc ( const UTF8 *pMailAliasDesc, size_t *pnValidMailAliasDesc, bool *pbValidMailAliasDesc, size_t *pnVisualWidth ) { *pnValidMailAliasDesc = 0; *pbValidMailAliasDesc = false; *pnVisualWidth = 0; if (!pMailAliasDesc) { return nullptr; } // Remove all '\r\n\t' from the string. // Terminate any ANSI in the string. // thread_local UTF8 szFittedMailAliasDesc[SIZEOF_MALIASDESC+1]; mux_field nValidMailAliasDesc = StripTabsAndTruncate ( pMailAliasDesc, szFittedMailAliasDesc, SIZEOF_MALIASDESC, WIDTHOF_MALIASDESC ); *pnValidMailAliasDesc = nValidMailAliasDesc.m_byte; *pbValidMailAliasDesc = true; *pnVisualWidth = nValidMailAliasDesc.m_column; return szFittedMailAliasDesc; } static void malias_read(FILE *fp, bool bConvert) { int count = getref(fp); if (count <= 0) { return; } LBuf buffer = LBuf_Src("malias_read"); // Build into a temporary so that on any error (truncated file, OOM mid-load) // the unique_ptr destructors (and malias_t dtor) automatically clean up // anything allocated so far. Only commit on full or best-effort success. std::vector> tmp; tmp.reserve(count); for (int i = 0; i < count; i++) { // Format is: "%d %d\n", &(m->owner), &(m->numrecep) // if (!fgets(reinterpret_cast(buffer.get()), LBUF_SIZE, fp)) { // Truncated flatfile: commit whatever we successfully read so far. STARTLOG(LOG_BUGS, "BUG", "MAIL"); log_text(T("Unexpected end of file. Mail bag truncated.")); ENDLOG; break; } auto m = std::make_unique(); UTF8 *p = reinterpret_cast(strchr(reinterpret_cast(buffer.get()), ' ')); m->owner = 0; int numrecep = 0; if (p) { m->owner = mux_atoi64(buffer); numrecep = mux_atoi64(p + 1); } // The format of @malias name is "N:\n". // size_t nLen = GetLineTrunc(buffer, LBUF_SIZE, fp); buffer[nLen - 1] = '\0'; // Get rid of trailing '\n'. UTF8 *pBufferUnicode; if (bConvert) { size_t nBufferUnicode; pBufferUnicode = ConvertToUTF8(reinterpret_cast(buffer.get()), &nBufferUnicode); } else { pBufferUnicode = buffer; } size_t nMailAlias; bool bMailAlias; UTF8 *pMailAlias = MakeCanonicalMailAlias(pBufferUnicode + 2, &nMailAlias, &bMailAlias); if (bMailAlias) { m->name.assign(reinterpret_cast(pMailAlias), nMailAlias); } else { m->name = "Invalid"; } // The format of the description is "D:\n" // nLen = GetLineTrunc(buffer, LBUF_SIZE, fp); if (bConvert) { size_t nBufferUnicode; pBufferUnicode = ConvertToUTF8(reinterpret_cast(buffer.get()), &nBufferUnicode); } else { pBufferUnicode = buffer; } size_t nMailAliasDesc; bool bMailAliasDesc; size_t nVisualWidth; UTF8 *pMailAliasDesc = MakeCanonicalMailAliasDesc(pBufferUnicode + 2, &nMailAliasDesc, &bMailAliasDesc, &nVisualWidth); if (bMailAliasDesc) { m->desc.assign(reinterpret_cast(pMailAliasDesc), nMailAliasDesc); m->desc_width = nVisualWidth; } else { m->desc = "Invalid Desc"; m->desc_width = 12; } if (numrecep > 0) { // Clamp a hostile/corrupt recipient count. if (numrecep > (LBUF_SIZE + 1) / 2) { numrecep = (LBUF_SIZE + 1) / 2; } m->list.reserve(numrecep); for (int j = 0; j < numrecep; j++) { int k = getref(fp); m->list.push_back(k); } } tmp.push_back(std::move(m)); } // Commit (replace) the table with what we loaded. malias = std::move(tmp); } static void load_malias(FILE *fp, bool bConvert) { UTF8 buffer[200]; getref(fp); if ( fgets(reinterpret_cast(buffer), sizeof(buffer), fp) && strcmp(reinterpret_cast(buffer), "*** Begin MALIAS ***\n") == 0) { malias_read(fp, bConvert); } else { Log.WriteString(T("ERROR: Couldn’t find Begin MALIAS." ENDLINE)); return; } } void load_mail(FILE *fp) { UTF8 nbuf1[8]; // Read the version number. // if (!fgets(reinterpret_cast(nbuf1), sizeof(nbuf1), fp)) { return; } if (strncmp(reinterpret_cast(nbuf1), "+V6", 3) == 0) { // Started v6 on 2007-MAR-13. // load_mail_V6(fp); } else if (strncmp(reinterpret_cast(nbuf1), "+V5", 3) == 0) { load_mail_V5(fp); } } void check_mail_expiration(void) { if (nullptr != mudstate.pIMailControl) { MUX_RESULT mr = mudstate.pIMailControl->ExpireMail(); if (MUX_SUCCEEDED(mr)) { return; } } // Negative values for expirations never expire. // if (0 > mudconf.mail_expiration) { return; } dbref thing; int expire_secs = mudconf.mail_expiration * 86400; CLinearTimeAbsolute ltaNow; ltaNow.GetLocal(); CLinearTimeAbsolute ltaMail; DO_WHOLE_DB(thing) { if (No_Mail_Expire(thing)) { continue; } MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (M_Safe(mp)) { continue; } const UTF8 *pMailTimeStr = utf8(mp->time); if (!ltaMail.SetString(pMailTimeStr)) { continue; } CLinearTimeDelta ltd(ltaMail, ltaNow); if (ltd.ReturnSeconds() <= expire_secs) { continue; } // Delete this one. // ml.RemoveItem(); } } } void check_mail(dbref player, int folder, bool silent) { if (nullptr != mudstate.pIMailControl) { MUX_RESULT mr = mudstate.pIMailControl->CheckMail(player, folder, silent); if (MUX_SUCCEEDED(mr)) { return; } } // Check for new @mail // int rc; // Read messages. int uc; // Unread messages. int cc; // Cleared messages. int gc; // urgent messages. // Just count messages // count_mail(player, folder, &rc, &uc, &cc); urgent_mail(player, folder, &gc); #ifdef MAIL_ALL_FOLDERS // Three lines rather than one (#1717). Four independent counts in one // sentence cannot be pluralised: MN_(s, p, n) chooses one form from one // n, and in Russian each of those nouns needs its own. Split so every // count governs its own sentence, which is the only shape gettext can // express -- and which also fixes "1 messages" in English. // raw_notify(player, tprintf(MN_("MAIL: %d message in folder %d [%s].", "MAIL: %d messages in folder %d [%s].", rc + uc), rc + uc, folder, get_folder_name(player, folder))); raw_notify(player, tprintf(MN_("MAIL: %d unread message.", "MAIL: %d unread messages.", uc), uc)); raw_notify(player, tprintf(MN_("MAIL: %d cleared message.", "MAIL: %d cleared messages.", cc), cc)); #else // MAIL_ALL_FOLDERS if (rc + uc > 0) { raw_notify(player, tprintf(MN_("MAIL: %d message in folder %d [%s].", "MAIL: %d messages in folder %d [%s].", rc + uc), rc + uc, folder, get_folder_name(player, folder))); raw_notify(player, tprintf(MN_("MAIL: %d unread message.", "MAIL: %d unread messages.", uc), uc)); raw_notify(player, tprintf(MN_("MAIL: %d cleared message.", "MAIL: %d cleared messages.", cc), cc)); } else if (!silent) { // Blank lines stay T("") (#1443); prose is M_ without embedded CR (#1419). // raw_notify(player, T("")); raw_notify(player, M_("MAIL: You have no mail.")); raw_notify(player, T("")); } if (gc > 0) { raw_notify(player, tprintf(MN_( "URGENT MAIL: You have %d urgent message in folder %d [%s].", "URGENT MAIL: You have %d urgent messages in folder %d [%s].", gc), gc, folder, get_folder_name(player, folder))); } #endif // MAIL_ALL_FOLDERS } static void do_malias_send ( dbref player, UTF8 *tolist, UTF8 *listto, UTF8 *senderlistto, UTF8 *subject, int number, mail_flag flags, bool silent ) { int nResult; malias_t *m = get_malias(player, tolist, &nResult); if (nResult == GMA_INVALIDFORM) { raw_notify(player, tprintf(M_("MAIL: I can’t figure out from ‘%s’ who you want to mail to."), tolist)); return; } else if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), tolist)); return; } // Parse the player list. // dbref vic; int k; for (k = 0; k < static_cast(m->list.size()); k++) { vic = m->list[k]; if (isPlayer(vic)) { send_mail(player, m->list[k], (m->list[k] == player) ? senderlistto : listto, subject, number, flags, silent); } else { // Complain about it. // UTF8 *pMail = tprintf(M_("Alias Error: Bad Player %d for %s"), vic, tolist); int iMail = add_mail_message(player, pMail); if (iMail != NOTHING) { send_mail(GOD, GOD, listto, subject, iMail, 0, silent); MessageReferenceDec(iMail); } } } } static void do_malias_create(dbref player, UTF8 *alias, UTF8 *tolist) { int nResult; get_malias(player, alias, &nResult); if (nResult == GMA_INVALIDFORM) { raw_notify(player, M_("MAIL: What alias do you want to create?.")); return; } else if (nResult == GMA_FOUND) { raw_notify(player, tprintf(M_("MAIL: Mail Alias ‘%s’ already exists."), alias)); return; } auto pt = std::make_unique(); // Parse the player list. // UTF8 *head = tolist; UTF8 *tail, spot; UTF8 *buff; dbref target; int added = 0; while (head && *head) { while (*head == ' ') { head++; } tail = head; while (*tail && *tail != ' ') { if (*tail == '"') { head++; tail++; while (*tail && *tail != '"') { tail++; } } if (*tail) { tail++; } } tail--; if (*tail != '"') { tail++; } spot = *tail; *tail = '\0'; // Now locate a target. // if (!mux_stricmp(head, T("me"))) { target = player; } else if (*head == '#') { target = mux_atoi64(head + 1); } else { target = lookup_player(player, head, true); } if (!Good_obj(target) || !isPlayer(target)) { raw_notify(player, M_("MAIL: No such player.")); } else { buff = unparse_object(player, target, false); raw_notify(player, tprintf(M_("MAIL: %s added to alias %s"), buff, alias)); pt->list.push_back(target); added++; free_lbuf(buff); } // Get the next recip. // *tail = spot; head = tail; if (*head == '"') { head++; } } size_t nValidMailAlias; bool bValidMailAlias; UTF8 *pValidMailAlias = MakeCanonicalMailAlias( alias + 1, &nValidMailAlias, &bValidMailAlias); if (!bValidMailAlias) { raw_notify(player, M_("MAIL: Invalid mail alias.")); // pt (and any recipients pushed) will be cleaned up automatically // when it goes out of scope. return; } // The Mail Alias Description is a superset of the Mail Alias, // so, the following code is not necessary unless the specification // of the Mail Alias Description becomes more restrictive at some // future time. // UTF8 *pValidMailAliasDesc = pValidMailAlias; size_t nValidMailAliasDesc = nValidMailAlias; pt->name.assign(reinterpret_cast(pValidMailAlias), nValidMailAlias); pt->owner = player; pt->desc.assign(reinterpret_cast(pValidMailAliasDesc), nValidMailAliasDesc); pt->desc_width = nValidMailAliasDesc; malias.push_back(std::move(pt)); sqlite_wt_sync_all_aliases(); raw_notify(player, tprintf(M_("MAIL: Alias set ‘%s’ defined."), alias)); } static void do_malias_list(dbref player, UTF8 *alias) { int nResult; malias_t *m = get_malias(player, alias, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), alias)); return; } if (nResult != GMA_FOUND) { return; } if (!ExpMail(player) && (player != m->owner) && !(God(m->owner))) { raw_notify(player, M_("MAIL: Permission denied.")); return; } UTF8 *buff = alloc_lbuf("do_malias_list"); UTF8 *bp = buff; safe_tprintf_str(buff, &bp, M_("MAIL: Alias *%s: "), m->name.c_str()); for (int i = static_cast(m->list.size()) - 1; i > -1; i--) { const UTF8 *p = Moniker(m->list[i]); if (strchr(reinterpret_cast(p), ' ')) { safe_chr('"', buff, &bp); safe_str(p, buff, &bp); safe_chr('"', buff, &bp); } else { safe_str(p, buff, &bp); } safe_chr(' ', buff, &bp); } *bp = '\0'; raw_notify(player, buff); free_lbuf(buff); } static void do_malias_list_all(dbref player) { std::vector visible; visible.reserve(malias.size()); for (size_t i = 0; i < malias.size(); i++) { malias_t *m = malias[i].get(); if (GOD == m->owner || m->owner == player || God(player)) { visible.push_back(m); } } // Use a comparator on raw pointers for the temp view. auto cmp = [](malias_t* a, malias_t* b) { return mux_stricmp(utf8(a->name), utf8(b->name)) < 0; }; std::sort(visible.begin(), visible.end(), cmp); // Mail alias list schema (#1667 Phase 4 B1) — shared with module: // name 12 / description 40 / owner 15 via mux_table_*. // static const size_t kAliasNameCols = 12; static const size_t kAliasDescCols = 40; static const size_t kAliasOwnerCols = 15; bool notified = false; for (malias_t *m : visible) { if (!notified) { UTF8 header[LBUF_SIZE]; size_t pos = 0; pos = mux_table_append_ljust(header, sizeof(header), pos, M_("Name"), kAliasNameCols); pos = mux_table_append_bytes(header, sizeof(header), pos, " "); pos = mux_table_append_ljust(header, sizeof(header), pos, M_("Description"), kAliasDescCols); pos = mux_table_append_bytes(header, sizeof(header), pos, " "); pos = mux_table_append_ljust(header, sizeof(header), pos, M_("Owner"), kAliasOwnerCols); raw_notify(player, header); notified = true; } UTF8 line[LBUF_SIZE]; size_t pos = 0; pos = mux_table_append_ljust(line, sizeof(line), pos, reinterpret_cast(m->name.c_str()), kAliasNameCols); pos = mux_table_append_bytes(line, sizeof(line), pos, " "); pos = mux_table_append_ljust(line, sizeof(line), pos, reinterpret_cast(m->desc.c_str()), kAliasDescCols); pos = mux_table_append_bytes(line, sizeof(line), pos, " "); pos = mux_table_append_ljust(line, sizeof(line), pos, Moniker(m->owner), kAliasOwnerCols); raw_notify(player, line); } raw_notify(player, M_("***** End of Mail Aliases *****")); } static void do_malias_switch(dbref player, UTF8 *a1, UTF8 *a2) { if (a1 && *a1) { if (a2 && *a2) { do_malias_create(player, a1, a2); } else { do_malias_list(player, a1); } } else { do_malias_list_all(player); } } static void do_mail_cc(dbref player, UTF8 *arg, bool bBlind) { if (!(Flags2(player) & PLAYER_MAILS)) { raw_notify(player, M_("MAIL: No mail message in progress.")); return; } if (!arg || !*arg) { raw_notify(player, M_("MAIL: I do not know whom you want to mail.")); return; } UTF8 *tolist = make_numlist(player, arg, bBlind); if (!tolist) { return; } UTF8 *fulllist = alloc_lbuf("do_mail_cc"); UTF8 *bp = fulllist; safe_str(tolist, fulllist, &bp); const UTF8 *pPlayerMailTo = atr_get_raw(player, A_MAILTO); if (pPlayerMailTo) { safe_chr(' ', fulllist, &bp); safe_str(pPlayerMailTo, fulllist, &bp); } *bp = '\0'; atr_add_raw(player, A_MAILTO, fulllist); UTF8 *names = make_namelist(player, fulllist); raw_notify(player, tprintf(M_("MAIL: You are sending mail to ‘%s’."), names)); free_lbuf(names); free_lbuf(tolist); free_lbuf(fulllist); } static void mail_to_list(dbref player, UTF8 *list, UTF8 *subject, UTF8 *message, int flags, bool silent) { if (!list) { return; } if (!*list) { free_lbuf(list); return; } // Construct a tolist which excludes all the Blind Carbon Copy (BCC) // recipients, and a senderlist which includes all recipients (so the // sender's copy retains the full BCC information). // UTF8 *tolist = alloc_lbuf("mail_to_list"); UTF8 *p = tolist; UTF8 *senderlist = alloc_lbuf("mail_to_list.senderlist"); UTF8 *sp = senderlist; UTF8 *tail; UTF8 *head = list; while (*head) { while (*head == ' ') { head++; } tail = head; while ( *tail && *tail != ' ') { if (*tail == '"') { head++; tail++; while ( *tail && *tail != '"') { tail++; } } if (*tail) { tail++; } } tail--; if (*tail != '"') { tail++; } // Append every token to senderlist (including BCC entries). // if (sp != senderlist) { safe_chr(' ', senderlist, &sp); } memcpy(sp, head, tail - head); sp += tail - head; if (*head != '!') { if (p != tolist) { safe_chr(' ', tolist, &p); } memcpy(p, head, tail-head); p += tail-head; } // Get the next recipient. // head = tail; if (*head == '"') { head++; } } *p = '\0'; *sp = '\0'; int number = add_mail_message(player, message); if (number != NOTHING) { UTF8 spot; head = list; while (*head) { while (' ' == *head) { head++; } tail = head; while ( *tail && *tail != ' ') { if (*tail == '"') { head++; tail++; while ( *tail && *tail != '"') { tail++; } } if (*tail) { tail++; } } tail--; if (*tail != '"') { tail++; } spot = *tail; *tail = '\0'; if (*head == '!') { head++; } if (*head == '*') { do_malias_send(player, head, tolist, senderlist, subject, number, flags, silent); } else { dbref target = mux_atoi64(head); if ( Good_obj(target) && isPlayer(target)) { send_mail(player, target, (target == player) ? senderlist : tolist, subject, number, flags, silent); } } // Get the next recipient. // *tail = spot; head = tail; if (*head == '"') { head++; } } MessageReferenceDec(number); } free_lbuf(senderlist); free_lbuf(tolist); free_lbuf(list); } static void do_mail_quick(dbref player, UTF8 *arg1, UTF8 *arg2) { if (!arg1 || !*arg1) { raw_notify(player, M_("MAIL: I don’t know who you want to mail.")); return; } if (!arg2 || !*arg2) { raw_notify(player, M_("MAIL: No message.")); return; } if (Flags2(player) & PLAYER_MAILS) { raw_notify(player, M_("MAIL: Mail message already in progress.")); return; } if ( !Wizard(player) && ThrottleMail(player)) { raw_notify(player, M_("MAIL: Too much @mail sent recently.")); return; } UTF8 *bufDest = alloc_lbuf("do_mail_quick"); UTF8 *bpSubject = bufDest; mux_strncpy(bpSubject, arg1, LBUF_SIZE-1); parse_to(&bpSubject, '/', 1); if (!bpSubject) { raw_notify(player, M_("MAIL: No subject.")); free_lbuf(bufDest); return; } mail_to_list(player, make_numlist(player, bufDest, false), bpSubject, arg2, 0, false); free_lbuf(bufDest); } // --------------------------------------------------------------------------- // do_mail_send_softcode: Entry point for mailsend() softcode function. // // Returns: 1 on success, or a static error string on failure. // const UTF8 *do_mail_send_softcode(dbref player, UTF8 *recipients, UTF8 *subject, UTF8 *message) { // #1191: When the mail module owns the store, send through SoftcodeSend // so headers/bodies land in the same map as @mail commands. // if (nullptr != mudstate.pIMailControl) { const UTF8 *err = nullptr; MUX_RESULT mr = mudstate.pIMailControl->SoftcodeSend(player, recipients, subject, message, &err); if (MUX_SUCCEEDED(mr)) { return nullptr; } return (nullptr != err) ? err : S_("#-1 MAIL SEND FAILED"); } // Resolve to the owning player — softcode may run on a non-player object. // player = Owner(player); if (!Good_obj(player) || !isPlayer(player)) { return S_("#-1 NOT A PLAYER"); } if (!recipients || !*recipients) { return S_("#-1 NO RECIPIENTS"); } if (!subject || !*subject) { return S_("#-1 NO SUBJECT"); } if (!message || !*message) { return S_("#-1 NO MESSAGE"); } if (Flags2(player) & PLAYER_MAILS) { return S_("#-1 MAIL ALREADY IN PROGRESS"); } if ( !Wizard(player) && ThrottleMail(player)) { return S_("#-1 TOO MUCH MAIL SENT"); } UTF8 *numlist = make_numlist(player, recipients, false); if (!numlist || !*numlist) { if (numlist) { free_lbuf(numlist); } return S_("#-1 NO VALID RECIPIENTS"); } mail_to_list(player, numlist, subject, message, 0, true); return nullptr; } static void do_expmail_stop(dbref player, int flags) { if ((Flags2(player) & PLAYER_MAILS) != PLAYER_MAILS) { raw_notify(player, M_("MAIL: No message started.")); return; } dbref aowner; dbref aflags; UTF8 *tolist = atr_get("do_expmail_stop.3854", player, A_MAILTO, & aowner, &aflags); if (*tolist == '\0') { raw_notify(player, M_("MAIL: No recipients.")); free_lbuf(tolist); } else { UTF8 *pMailMsg = atr_get("do_expmail_stop.3862", player, A_MAILMSG, &aowner, &aflags); if (*pMailMsg == '\0') { raw_notify(player, M_("MAIL: The body of this message is empty. Use - to add to the message.")); free_lbuf(tolist); } else { UTF8 *mailsub = atr_get("do_expmail_stop.3870", player, A_MAILSUB, &aowner, &aflags); UTF8 *mailflags = atr_get("do_expmail_stop.3871", player, A_MAILFLAGS, &aowner, &aflags); mail_to_list(player, tolist, mailsub, pMailMsg, flags | mux_atoi64(mailflags), false); free_lbuf(mailflags); free_lbuf(mailsub); Flags2(player) &= ~PLAYER_MAILS; } free_lbuf(pMailMsg); } } static void do_expmail_abort(dbref player) { Flags2(player) &= ~PLAYER_MAILS; raw_notify(player, M_("MAIL: Message aborted.")); } void do_prepend(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *text, const UTF8 *cargs[], int ncargs) { UNUSED_PARAMETER(key); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); if (Flags2(executor) & PLAYER_MAILS) { if ( !text || !*text) { raw_notify(executor, M_("No text prepended.")); return; } UTF8 *bufText = alloc_lbuf("do_prepend"); UTF8 *bpText = bufText; mux_exec(text+1, LBUF_SIZE-1, bufText, &bpText, executor, caller, enactor, eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, nullptr, 0); *bpText = '\0'; dbref aowner; int aflags; UTF8 *oldmsg = atr_get("do_prepend.3915", executor, A_MAILMSG, &aowner, &aflags); if (*oldmsg) { UTF8 *newmsg = alloc_lbuf("do_prepend"); UTF8 *bp = newmsg; safe_str(bufText, newmsg, &bp); safe_chr(' ', newmsg, &bp); safe_str(oldmsg, newmsg, &bp); *bp = '\0'; atr_add_raw(executor, A_MAILMSG, newmsg); free_lbuf(newmsg); } else { atr_add_raw(executor, A_MAILMSG, bufText); } free_lbuf(bufText); free_lbuf(oldmsg); size_t nLen; atr_get_raw_LEN(executor, A_MAILMSG, &nLen); raw_notify(executor, tprintf(M_("%d/%d characters prepended."), nLen, LBUF_SIZE-1)); } else { raw_notify(executor, M_("MAIL: No message in progress.")); } } void do_postpend(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *text, const UTF8 *cargs[], int ncargs) { UNUSED_PARAMETER(key); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); if ( text[1] == '-' && text[2] == '\0') { do_expmail_stop(executor, 0); return; } if (Flags2(executor) & PLAYER_MAILS) { if ( !text || !*text) { raw_notify(executor, M_("No text added.")); return; } UTF8 *bufText = alloc_lbuf("do_prepend"); UTF8 *bpText = bufText; mux_exec(text+1, LBUF_SIZE-1, bufText, &bpText, executor, caller, enactor, eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, nullptr, 0); *bpText = '\0'; dbref aowner; int aflags; UTF8 *oldmsg = atr_get("do_postpend.3978", executor, A_MAILMSG, &aowner, &aflags); if (*oldmsg) { UTF8 *newmsg = alloc_lbuf("do_postpend"); UTF8 *bp = newmsg; safe_str(oldmsg, newmsg, &bp); safe_chr(' ', newmsg, &bp); safe_str(bufText, newmsg, &bp); *bp = '\0'; atr_add_raw(executor, A_MAILMSG, newmsg); free_lbuf(newmsg); } else { atr_add_raw(executor, A_MAILMSG, bufText); } free_lbuf(bufText); free_lbuf(oldmsg); size_t nLen; atr_get_raw_LEN(executor, A_MAILMSG, &nLen); raw_notify(executor, tprintf(M_("%d/%d characters added."), nLen, LBUF_SIZE-1)); } else { raw_notify(executor, M_("MAIL: No message in progress.")); } } static void do_edit_msg(dbref player, UTF8 *from, UTF8 *to) { if (Flags2(player) & PLAYER_MAILS) { dbref aowner; int aflags; UTF8 *msg = atr_get("do_edit_msg.4014", player, A_MAILMSG, &aowner, &aflags); UTF8 *result = replace_string(from, to, msg); atr_add(player, A_MAILMSG, result, aowner, aflags); raw_notify(player, M_("Text edited.")); free_lbuf(result); free_lbuf(msg); } else { raw_notify(player, M_("MAIL: No message in progress.")); } } static void do_mail_proof(dbref player) { if (!(Flags2(player) & PLAYER_MAILS)) { raw_notify(player, M_("MAIL: No message in progress.")); return; } dbref aowner; int aflags; UTF8 *mailto = atr_get("do_mail_proof.4038", player, A_MAILTO, &aowner, &aflags); UTF8 *pMailMsg = atr_get("do_mail_proof.4039", player, A_MAILMSG, &aowner, &aflags); UTF8 *names = make_namelist(player, mailto); UTF8 szSubjectBuffer[MBUF_SIZE]; StripTabsAndTruncate( atr_get_raw(player, A_MAILSUB), szSubjectBuffer, MBUF_SIZE-1, 35); UTF8 szFromName[MBUF_SIZE]; trimmed_name(player, szFromName, 16, 16, 0); raw_notify(player, DASH_LINE); raw_notify(player, tprintf(M_("From: %s Subject: %s\nTo: %s"), szFromName, szSubjectBuffer, names)); raw_notify(player, DASH_LINE); raw_notify(player, pMailMsg); raw_notify(player, DASH_LINE); free_lbuf(pMailMsg); free_lbuf(names); free_lbuf(mailto); } static void do_malias_desc(dbref player, UTF8 *alias, UTF8 *desc) { int nResult; malias_t *m = get_malias(player, alias, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), alias)); return; } if (nResult != GMA_FOUND) { return; } if ( m->owner != GOD || ExpMail(player)) { size_t nValidMailAliasDesc; bool bValidMailAliasDesc; size_t nVisualWidth; UTF8 *pValidMailAliasDesc = MakeCanonicalMailAliasDesc ( desc, &nValidMailAliasDesc, &bValidMailAliasDesc, &nVisualWidth ); if (bValidMailAliasDesc) { m->desc.assign(reinterpret_cast(pValidMailAliasDesc), nValidMailAliasDesc); m->desc_width = nVisualWidth; sqlite_wt_sync_all_aliases(); raw_notify(player, M_("MAIL: Description changed.")); } else { raw_notify(player, M_("MAIL: Description is not valid.")); } } else { raw_notify(player, M_("MAIL: Permission denied.")); } } static void do_malias_chown(dbref player, UTF8 *alias, UTF8 *owner) { if (!ExpMail(player)) { raw_notify(player, M_("MAIL: You cannot do that!")); return; } int nResult; malias_t *m = get_malias(player, alias, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), alias)); return; } if (nResult != GMA_FOUND) { return; } dbref no = lookup_player(player, owner, true); if (no == NOTHING) { raw_notify(player, M_("MAIL: I do not see that here.")); return; } m->owner = no; sqlite_wt_sync_all_aliases(); raw_notify(player, M_("MAIL: Owner changed for alias.")); } static void do_malias_add(dbref player, UTF8 *alias, UTF8 *person) { int nResult; malias_t *m = get_malias(player, alias, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), alias)); return; } else if (nResult != GMA_FOUND) { return; } dbref thing = NOTHING; if (*person == '#') { thing = parse_dbref(person + 1); if (!isPlayer(thing)) { raw_notify(player, M_("MAIL: Only players may be added.")); return; } } if (thing == NOTHING) { thing = lookup_player(player, person, true); } if (thing == NOTHING) { raw_notify(player, M_("MAIL: I do not see that person here.")); return; } if ((m->owner == GOD) && !ExpMail(player)) { raw_notify(player, M_("MAIL: Permission denied.")); return; } for (size_t i = 0; i < m->list.size(); i++) { if (m->list[i] == thing) { raw_notify(player, M_("MAIL: That person is already on the list.")); return; } } m->list.push_back(thing); sqlite_wt_sync_all_aliases(); raw_notify(player, tprintf(M_("MAIL: %s added to %s"), Moniker(thing), m->name.c_str())); } static void do_malias_remove(dbref player, UTF8 *alias, UTF8 *person) { int nResult; malias_t *m = get_malias(player, alias, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), alias)); return; } if (nResult != GMA_FOUND) { return; } if ((m->owner == GOD) && !ExpMail(player)) { raw_notify(player, M_("MAIL: Permission denied.")); return; } dbref thing = NOTHING; if (*person == '#') { thing = parse_dbref(person + 1); } if (thing == NOTHING) { thing = lookup_player(player, person, true); } if (thing == NOTHING) { raw_notify(player, M_("MAIL: I do not see that person here.")); return; } auto it = std::find(m->list.begin(), m->list.end(), thing); bool ok = (it != m->list.end()); if (ok) { m->list.erase(it); sqlite_wt_sync_all_aliases(); raw_notify(player, tprintf(M_("MAIL: %s removed from alias %s."), Moniker(thing), alias)); } else { raw_notify(player, tprintf(M_("MAIL: %s is not a member of alias %s."), Moniker(thing), alias)); } } static void do_malias_rename(dbref player, UTF8 *alias, UTF8 *newname) { int nResult; malias_t *m = get_malias(player, newname, &nResult); if (nResult == GMA_FOUND) { raw_notify(player, M_("MAIL: That name already exists!")); return; } if (nResult != GMA_NOTFOUND) { return; } m = get_malias(player, alias, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, M_("MAIL: I cannot find that alias!")); return; } if (nResult != GMA_FOUND) { return; } if (!ExpMail(player) && !(m->owner == player)) { raw_notify(player, M_("MAIL: Permission denied.")); return; } size_t nValidMailAlias; bool bValidMailAlias; UTF8 *pValidMailAlias = MakeCanonicalMailAlias ( newname+1, &nValidMailAlias, &bValidMailAlias ); if (bValidMailAlias) { m->name.assign(reinterpret_cast(pValidMailAlias), nValidMailAlias); sqlite_wt_sync_all_aliases(); raw_notify(player, M_("MAIL: Mailing Alias renamed.")); } else { raw_notify(player, M_("MAIL: Alias is not valid.")); } } static void do_malias_delete(dbref player, UTF8 *alias) { int nResult; malias_t *m = get_malias(player, alias, &nResult); if (nResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), alias)); return; } if (nResult != GMA_FOUND) { return; } for (auto it = malias.begin(); it != malias.end(); ++it) { if (it->get() == m) { if ((m->owner == player) || ExpMail(player)) { malias.erase(it); sqlite_wt_sync_all_aliases(); raw_notify(player, M_("MAIL: Alias Deleted.")); return; } break; } } raw_notify(player, tprintf(M_("MAIL: Alias ‘%s’ not found."), alias)); } static void do_malias_adminlist(dbref player) { if (!ExpMail(player)) { do_malias_list_all(player); return; } // Mail alias admin list (#1667 Phase 4 B2) — same widths as B1 plus // a 4-column index prefix. // static const size_t kAliasNumCols = 4; static const size_t kAliasNameCols = 12; static const size_t kAliasDescCols = 40; static const size_t kAliasOwnerCols = 15; { UTF8 header[LBUF_SIZE]; size_t pos = 0; pos = mux_table_append_ljust(header, sizeof(header), pos, M_("Num"), kAliasNumCols); pos = mux_table_append_bytes(header, sizeof(header), pos, " "); pos = mux_table_append_ljust(header, sizeof(header), pos, M_("Name"), kAliasNameCols); pos = mux_table_append_bytes(header, sizeof(header), pos, " "); pos = mux_table_append_ljust(header, sizeof(header), pos, M_("Description"), kAliasDescCols); pos = mux_table_append_bytes(header, sizeof(header), pos, " "); pos = mux_table_append_ljust(header, sizeof(header), pos, M_("Owner"), kAliasOwnerCols); raw_notify(player, header); } for (size_t i = 0; i < malias.size(); i++) { malias_t *m = malias[i].get(); UTF8 line[LBUF_SIZE]; size_t pos = 0; UTF8 numbuf[8]; mux_sprintf(numbuf, sizeof(numbuf), T("%d"), static_cast(i)); pos = mux_table_append_ljust(line, sizeof(line), pos, numbuf, kAliasNumCols); pos = mux_table_append_bytes(line, sizeof(line), pos, " "); pos = mux_table_append_ljust(line, sizeof(line), pos, reinterpret_cast(m->name.c_str()), kAliasNameCols); pos = mux_table_append_bytes(line, sizeof(line), pos, " "); pos = mux_table_append_ljust(line, sizeof(line), pos, reinterpret_cast(m->desc.c_str()), kAliasDescCols); pos = mux_table_append_bytes(line, sizeof(line), pos, " "); pos = mux_table_append_ljust(line, sizeof(line), pos, Moniker(m->owner), kAliasOwnerCols); raw_notify(player, line); } raw_notify(player, M_("***** End of Mail Aliases *****")); } static void do_malias_status(dbref player) { if (!ExpMail(player)) { raw_notify(player, M_("MAIL: Permission denied.")); } else { raw_notify(player, tprintf(M_("MAIL: Number of mail aliases defined: %d"), static_cast(malias.size()))); raw_notify(player, tprintf(M_("MAIL: Vector capacity: %d"), static_cast(malias.capacity()))); } } XFUNCTION(fun_malias) { UNUSED_PARAMETER(fp); UNUSED_PARAMETER(caller); UNUSED_PARAMETER(enactor); UNUSED_PARAMETER(eval); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); dbref target; if (nfargs == 0) { target = executor; } else { target = match_thing_quiet(executor, fargs[0]); if (!Good_obj(target)) { safe_match_result(target, buff, bufc); return; } } if ( target != executor && !ExpMail(executor)) { safe_noperm(buff, bufc); return; } ITL pContext; ItemToList_Init(&pContext, buff, bufc); for (size_t i = 0; i < malias.size(); i++) { malias_t *m = malias[i].get(); if (m->owner == target) { if (!ItemToList_AddString(&pContext, utf8(m->name))) { break; } } } ItemToList_Final(&pContext); } static void malias_cleanup1(malias_t *m, dbref target) { m->list.erase( std::remove_if(m->list.begin(), m->list.end(), [target](dbref j) { return !Good_obj(j) || j == target; }), m->list.end()); } void malias_cleanup(dbref player) { // Remove destroyed player from alias membership lists. for (size_t i = 0; i < malias.size(); i++) { malias_cleanup1(malias[i].get(), player); } // Delete aliases owned by the destroyed player. Iterate backwards. for (int i = static_cast(malias.size()) - 1; i >= 0; i--) { malias_t *m = malias[i].get(); if (m->owner == player) { malias.erase(malias.begin() + i); } } sqlite_wt_sync_all_aliases(); } static void do_mail_retract1(dbref player, UTF8 *name, UTF8 *msglist) { dbref target = lookup_player(player, name, true); if (target == NOTHING) { raw_notify(player, M_("MAIL: No such player.")); return; } struct mail_selector ms; if (!parse_msglist(msglist, &ms, target)) { return; } int i = 0, j = 0; MailList ml(target); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (mail_from_player(player, mp)) { i++; if (mail_match(mp, ms, i)) { j++; if (Unread(mp)) { ml.RemoveItem(); raw_notify(player, M_("MAIL: Mail retracted.")); } else { raw_notify(player, M_("MAIL: That message has been read.")); } } } } if (!j) { // Ran off the end of the list without finding anything. // raw_notify(player, M_("MAIL: No matching messages.")); } } static void do_mail_retract(dbref player, UTF8 *name, UTF8 *msglist) { if (*name == '*') { int pnResult; malias_t *m = get_malias(player, name, &pnResult); if (pnResult == GMA_NOTFOUND) { raw_notify(player, tprintf(M_("MAIL: Mail alias %s not found."), name)); return; } if (pnResult == GMA_FOUND) { for (size_t i = 0; i < m->list.size(); i++) { do_mail_retract1(player, tprintf(T("#%d"), m->list[i]), msglist); } } } else { do_mail_retract1(player, name, msglist); } } void do_malias ( dbref executor, dbref caller, dbref enactor, int eval, int key, int nargs, UTF8 *arg1, UTF8 *arg2, const UTF8 *cargs[], int ncargs ) { UNUSED_PARAMETER(caller); UNUSED_PARAMETER(enactor); UNUSED_PARAMETER(eval); UNUSED_PARAMETER(nargs); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); if (nullptr != mudstate.pIMailControl) { // #1198: with the module loaded, never fall through into the engine // malias store (dual-store split). // MUX_RESULT mr = mudstate.pIMailControl->MaliasCommand(executor, key, arg1, arg2); if (MUX_SUCCEEDED(mr)) { return; } if (MUX_E_NOTIMPLEMENTED == mr) { raw_notify(executor, M_("MAIL: That @malias command is not available.")); } return; } switch (key) { case 0: do_malias_switch(executor, arg1, arg2); break; case MALIAS_DESC: do_malias_desc(executor, arg1, arg2); break; case MALIAS_CHOWN: do_malias_chown(executor, arg1, arg2); break; case MALIAS_ADD: do_malias_add(executor, arg1, arg2); break; case MALIAS_REMOVE: do_malias_remove(executor, arg1, arg2); break; case MALIAS_DELETE: do_malias_delete(executor, arg1); break; case MALIAS_RENAME: do_malias_rename(executor, arg1, arg2); break; case 7: // empty break; case MALIAS_LIST: do_malias_adminlist(executor); break; case MALIAS_STATUS: do_malias_status(executor); } } void do_mail ( dbref executor, dbref caller, dbref enactor, int eval, int key, int nargs, UTF8 *arg1, UTF8 *arg2, const UTF8 *cargs[], int ncargs ) { UNUSED_PARAMETER(caller); UNUSED_PARAMETER(enactor); UNUSED_PARAMETER(eval); UNUSED_PARAMETER(nargs); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); if (nullptr != mudstate.pIMailControl) { // #1198: with the module loaded, never fall through into engine // mail_storage — that mutates a second world for the same process. // MUX_RESULT mr = mudstate.pIMailControl->MailCommand(executor, key, arg1, arg2); if (MUX_SUCCEEDED(mr)) { return; } if (MUX_E_NOTIMPLEMENTED == mr) { raw_notify(executor, M_("MAIL: That @mail command is not available.")); } return; } // Only @mail/quick is allowed from non-player executors. All other // @mail subcommands use interactive state (composing buffer, folder // selection, etc.) that only makes sense in a player session. // if ( (key & ~MAIL_QUOTE) != MAIL_QUICK && !isPlayer(executor)) { return; } switch (key & ~MAIL_QUOTE) { case 0: do_mail_stub(executor, arg1, arg2); break; case MAIL_STATS: do_mail_stats(executor, arg1, 0); break; case MAIL_DSTATS: do_mail_stats(executor, arg1, 1); break; case MAIL_FSTATS: do_mail_stats(executor, arg1, 2); break; case MAIL_DEBUG: do_mail_debug(executor, arg1, arg2); break; case MAIL_NUKE: do_mail_nuke(executor); break; case MAIL_FOLDER: do_mail_change_folder(executor, arg1, arg2); break; case MAIL_LIST: do_mail_list(executor, arg1, arg2, false); break; case MAIL_READ: do_mail_read(executor, arg1, arg2); break; case MAIL_CLEAR: do_mail_clear(executor, arg1); break; case MAIL_UNCLEAR: do_mail_unclear(executor, arg1); break; case MAIL_PURGE: do_mail_purge(executor); break; case MAIL_FILE: do_mail_file(executor, arg1, arg2); break; case MAIL_TAG: do_mail_tag(executor, arg1); break; case MAIL_UNTAG: do_mail_untag(executor, arg1); break; case MAIL_FORWARD: do_mail_fwd(executor, arg1, arg2); break; case MAIL_REPLY: do_mail_reply(executor, arg1, false, key); break; case MAIL_REPLYALL: do_mail_reply(executor, arg1, true, key); break; case MAIL_SEND: do_expmail_stop(executor, 0); break; case MAIL_EDIT: do_edit_msg(executor, arg1, arg2); break; case MAIL_URGENT: do_expmail_stop(executor, M_URGENT); break; case MAIL_ALIAS: do_malias_create(executor, arg1, arg2); break; case MAIL_ALIST: do_malias_list_all(executor); break; case MAIL_PROOF: do_mail_proof(executor); break; case MAIL_ABORT: do_expmail_abort(executor); break; case MAIL_QUICK: do_mail_quick(executor, arg1, arg2); break; case MAIL_REVIEW: do_mail_review(executor, arg1, arg2); break; case MAIL_RETRACT: do_mail_retract(executor, arg1, arg2); break; case MAIL_CC: do_mail_cc(executor, arg1, false); break; case MAIL_SAFE: do_mail_safe(executor, arg1); break; case MAIL_UNSAFE: do_mail_unsafe(executor, arg1); break; case MAIL_BCC: do_mail_cc(executor, arg1, true); break; case MAIL_NEXT: do_mail_next(executor); break; } } struct mail *MailList::FirstItem(void) { ensure_mail_softcode_sync(); auto it = mail_storage.find(m_player); if (it == mail_storage.end() || it->second.empty()) { m_mi = {}; m_miEnd = {}; m_bRemoved = false; return nullptr; } auto& lst = it->second; m_mi = lst.begin(); m_miEnd = lst.end(); m_bRemoved = false; return &*m_mi; } struct mail *MailList::NextItem(void) { if (!m_bRemoved) { if (m_mi != m_miEnd) { ++m_mi; if (m_mi == m_miEnd) { m_mi = m_miEnd; } } } m_bRemoved = false; if (m_mi == m_miEnd) return nullptr; return &*m_mi; } bool MailList::IsEnd(void) { return (m_mi == m_miEnd); } MailList::MailList(dbref player) { m_player = player; m_bRemoved = false; } void MailList::RemoveItem(void) { if (m_mi == m_miEnd || NOTHING == m_player) { return; } auto it = mail_storage.find(m_player); if (it == mail_storage.end()) return; auto& lst = it->second; sqlite_wt_delete_mail(&*m_mi); MessageReferenceDec(m_mi->number); auto next_it = lst.erase(m_mi); m_bRemoved = true; // If this was the last message, clear the iterators and erase the map // entry. Clearing is the point: leaving m_mi/m_miEnd pointing into an // erased std::list makes IsEnd()/NextItem() dereference invalidated // iterators (purge, expire, retract-unread, mail-debug). RemoveAll() // clears the same pair, but erases first; here we must test lst.empty() // first, so the list has to stay alive until after the test -- hence // clear, then erase. // if (lst.empty()) { m_mi = {}; m_miEnd = {}; mail_storage.erase(it); } else { m_mi = next_it; } } void MailList::AppendItem(mail &&miNew) { auto& lst = mail_storage[m_player]; lst.push_back(std::move(miNew)); } void MailList::RemoveAll(void) { auto it = mail_storage.find(m_player); if (it == mail_storage.end()) return; auto& lst = it->second; for (auto& m : lst) { MessageReferenceDec(m.number); } sqlite_wt_delete_all_mail(m_player); mail_storage.erase(it); m_mi = {}; m_miEnd = {}; } static void ListMailInFolderNumber(dbref player, int folder_num, UTF8 *msglist) { int original_folder = player_folder(player); set_player_folder(player, folder_num); struct mail_selector ms; if (!parse_msglist(msglist, &ms, player)) { return; } int i = 0; raw_notify(player, tprintf(T(FOLDER_LINE), folder_num)); MailList ml(player); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { if (Folder(mp) == folder_num) { i++; if (mail_match(mp, ms, i)) { size_t nSize = MessageFetchSize(mp->number); UTF8 szFromName[MBUF_SIZE]; trimmed_name(mp->from, szFromName, 16, 16, 0); UTF8 line[LBUF_SIZE]; format_mail_list_line_sub(line, sizeof(line), status_chars(mp), i, nSize, szFromName, utf8(mp->subject)); raw_notify(player, line); } } } raw_notify(player, DASH_LINE); set_player_folder(player, original_folder); } static void ListMailInFolder(dbref player, UTF8 *folder_name, UTF8 *msglist) { int folder = 0; if ( nullptr == folder_name || '\0' == folder_name[0]) { folder = player_folder(player); } else { folder = parse_folder(player, folder_name); } if (-1 == folder) { raw_notify(player, M_("MAIL: No such folder.")); return; } ListMailInFolderNumber(player, folder, msglist); } void do_folder ( dbref executor, dbref caller, dbref enactor, int eval, int key, int nargs, UTF8 *arg1, UTF8 *arg2, const UTF8 *cargs[], int ncargs ) { UNUSED_PARAMETER(caller); UNUSED_PARAMETER(enactor); UNUSED_PARAMETER(eval); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); if (nullptr != mudstate.pIMailControl) { // #1198: with the module loaded, never fall through into the engine store. // MUX_RESULT mr = mudstate.pIMailControl->FolderCommand(executor, key, nargs, arg1, arg2); if (MUX_SUCCEEDED(mr)) { return; } if (MUX_E_NOTIMPLEMENTED == mr) { raw_notify(executor, M_("MAIL: That folder command is not available.")); } return; } switch (key) { case FOLDER_FILE: do_mail_file(executor, arg1, arg2); break; case FOLDER_LIST: ListMailInFolder(executor, arg1, arg2); break; case FOLDER_READ: do_mail_read(executor, arg1, arg2); break; case FOLDER_SET: do_mail_change_folder(executor, arg1, arg2); break; default: if ( nullptr == arg1 || '\0' == arg1[0]) { DoListMailBrief(executor); } else if (2 == nargs) { do_mail_read(executor, arg1, arg2); } else { do_mail_change_folder(executor, arg1, arg2); } break; } } // --------------------------------------------------------------------------- // SQLite mail bulk sync and load (Phase 1). // --------------------------------------------------------------------------- static void clear_runtime_mail_data(void) { mail_storage.clear(); mail_list.clear(); mudstate.mail_db_top = 0; malias.clear(); } bool sqlite_sync_mail(void) { CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.Begin()) { return false; } if (!sqldb.ClearMailTables()) { sqldb.Rollback(); return false; } // Sync mail bodies first (mail_headers.body_number references // mail_bodies.number via foreign key). // for (int i = 0; i < mudstate.mail_db_top; i++) { if (0 < mail_list[i].m_nRefs) { if (!sqldb.SyncMailBody(i, utf8(mail_list[i].m_pMessage))) { sqldb.Rollback(); return false; } } } // Sync mail headers. // dbref thing; DO_WHOLE_DB(thing) { if (isPlayer(thing)) { MailList ml(thing); struct mail *mp; for (mp = ml.FirstItem(); !ml.IsEnd(); mp = ml.NextItem()) { mp->sqlite_id = sqldb.InsertMailHeaderReturningId( mp->to, mp->from, mp->number, utf8(mp->tolist), utf8(mp->time), utf8(mp->subject), mp->read); if (mp->sqlite_id < 0) { sqldb.Rollback(); return false; } } } } // Sync mail aliases. // for (size_t i = 0; i < malias.size(); i++) { malias_t *m = malias[i].get(); // Serialize member list to space-separated string. // LBuf members_buf = LBuf_Src("sync_mail_alias"); UTF8 *bp = members_buf; for (size_t j = 0; j < m->list.size(); j++) { if (j > 0) { safe_chr(' ', members_buf, &bp); } safe_str(tprintf(T("%d"), m->list[j]), members_buf, &bp); } *bp = '\0'; if (!sqldb.SyncMailAlias(m->owner, utf8(m->name), utf8(m->desc), static_cast(m->desc_width), members_buf)) { sqldb.Rollback(); return false; } } if (!sqldb.PutMeta("mail_db_top", mudstate.mail_db_top)) { sqldb.Rollback(); return false; } if (!sqldb.Commit()) { sqldb.Rollback(); return false; } return true; } int sqlite_load_mail(void) { mudstate.bSQLiteLoading = true; CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); int mail_top = 0; CSQLiteDB::MetaGetResult mail_top_meta = sqldb.GetMetaEx("mail_db_top", &mail_top); if (CSQLiteDB::MetaGetResult::Error == mail_top_meta) { mudstate.bSQLiteLoading = false; return -1; } if (CSQLiteDB::MetaGetResult::Found != mail_top_meta) { mudstate.bSQLiteLoading = false; return 0; } clear_runtime_mail_data(); mail_db_grow(mail_top + 1); // Load mail bodies first (they must exist before headers reference them). // if (!sqldb.LoadAllMailBodies([](int number, const UTF8 *message) { new_mail_message(const_cast(message), number); })) { clear_runtime_mail_data(); mudstate.bSQLiteLoading = false; return -1; } // Load mail headers. // if (!sqldb.LoadAllMailHeaders([](int64_t rowid, int to_player, int from_player, int body_number, const UTF8 *tolist, const UTF8 *time_str, const UTF8 *subject, int read_flags) { auto& lst = mail_storage[to_player]; lst.emplace_back(); mail& m = lst.back(); m.to = to_player; m.from = from_player; m.number = body_number; MessageReferenceInc(m.number); m.tolist.assign(reinterpret_cast(tolist ? tolist : reinterpret_cast(""))); m.time.assign(reinterpret_cast(time_str ? time_str : reinterpret_cast(""))); m.subject.assign(reinterpret_cast(subject ? subject : reinterpret_cast(""))); m.read = read_flags; m.sqlite_id = rowid; })) { clear_runtime_mail_data(); mudstate.bSQLiteLoading = false; return -1; } // Load mail aliases. // std::vector> alias_vec; auto free_alias_vec = [&alias_vec]() { alias_vec.clear(); }; if (!sqldb.LoadAllMailAliases([&alias_vec](int owner, const UTF8 *name, const UTF8 *desc, int desc_width, const UTF8 *members) { auto m = std::make_unique(); m->owner = owner; m->name = name ? reinterpret_cast(name) : ""; m->desc = desc ? reinterpret_cast(desc) : ""; m->desc_width = desc_width; // Parse space-separated member list. // m->list.clear(); if (members && members[0] != '\0') { LBuf buf = LBuf_Src("load_mail_alias"); mux_strncpy(buf, members, LBUF_SIZE - 1); UTF8 *p = buf; while (*p) { while (*p == ' ') p++; if (*p == '\0') break; m->list.push_back(mux_atoi64(p)); while (*p && *p != ' ') p++; } } alias_vec.push_back(std::move(m)); })) { free_alias_vec(); clear_runtime_mail_data(); mudstate.bSQLiteLoading = false; return -1; } if (!alias_vec.empty()) { malias.reserve(alias_vec.size()); for (auto& up : alias_vec) { malias.push_back(std::move(up)); } alias_vec.clear(); } mudstate.bSQLiteLoading = false; return 1; }