tinymux/mux/modules/engine/mail.cpp
Stephen Dennis 8808b4c375 feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced
The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`.  Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.

The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.

New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.

The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper).  const_cast budget: zero
added.

Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW).  delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup.  After any signature flip, grep the
old spelling.

Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:34:23 -06:00

6072 lines
162 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*! \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 <algorithm>
#include <memory>
#include <string>
#include <vector>
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<dbref> list;
malias_t() : owner(NOTHING), desc_width(0) {}
};
static std::vector<std::unique_ptr<malias_t>> malias;
static std::vector<MAILBODY> mail_list;
// Per-player mail lists using STL for ownership and iteration.
static std::unordered_map<dbref, std::list<mail>> 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<const UTF8*>(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<long long>(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<long long>(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<int>(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<int>(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<char *>(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<const char *>(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<char *>(pMessage));
MAILBODY &pm = mail_list[i];
pm.m_pMessage.assign(reinterpret_cast<const char *>(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<char *>(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<char *>(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<char *>(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<UTF8 *>(strchr(reinterpret_cast<char *>(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 isnt enough (all?)"));
return false;
case 'l':
case 'L':
// All messages, all folders
//
p++;
switch (*p)
{
case '\0':
raw_notify(player, M_("MAIL: AL isnt 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<const char *>(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<char *>(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 dont 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 dont 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<int>(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<char *>(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 *<name> or #<num>."));
}
else
{
raw_notify(player, M_("MAIL: Mail aliases must be of the form *<name>."));
}
}
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*>(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 dont 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*>(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<const char *>(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<const char *>(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<const char *>(
(nullptr != time_str) ? time_str : T("")));
pos = mux_table_append_bytes(line, nLine, pos, " ");
pos = mux_table_append_bytes(line, nLine, pos,
reinterpret_cast<const char *>(
(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 dont 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 dont 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<int>(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<int>(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 dont understand that message number."));
return;
}
struct mail *mp = mail_fetch(player, num);
if (!mp)
{
raw_notify(player, M_("MAIL: You cant 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 dont understand that message number."));
return;
}
struct mail *mp = mail_fetch(player, num);
if (!mp)
{
raw_notify(player, M_("MAIL: You cant 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<const char *>(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 cant return your mail."), Moniker(target)));
return true;
}
else
{
raw_notify(player, tprintf(M_("Sorry, %s cant 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<const char *>(tolist));
}
newm.number = number;
MessageReferenceInc(number);
newm.time.assign(reinterpret_cast<const char *>(pTimeStr));
newm.subject.assign(reinterpret_cast<const char *>(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: %ss mailbox is full (%d message).",
"MAIL: %ss 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<int> 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<int> 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 <player>, 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<char *>(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<int>(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<int>(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<UTF8 *>(fgets(reinterpret_cast<char *>(nbuf1), sizeof(nbuf1), fp));
while ( nullptr != p
&& strncmp(reinterpret_cast<char *>(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<UTF8 *>(getstring_noalloc(fp, true, &nBuffer));
m.tolist.assign(reinterpret_cast<const char *>(pBuffer), nBuffer);
pBuffer = reinterpret_cast<UTF8 *>(getstring_noalloc(fp, true, &nBuffer));
m.time.assign(reinterpret_cast<const char *>(pBuffer), nBuffer);
pBuffer = reinterpret_cast<UTF8 *>(getstring_noalloc(fp, true, &nBuffer));
m.subject.assign(reinterpret_cast<const char *>(pBuffer), nBuffer);
m.read = getref(fp);
m.sqlite_id = -1;
p = reinterpret_cast<UTF8 *>(fgets(reinterpret_cast<char *>(nbuf1), sizeof(nbuf1), fp));
}
p = reinterpret_cast<UTF8 *>(fgets(reinterpret_cast<char *>(nbuf1), sizeof(nbuf1), fp));
while ( nullptr != p
&& strncmp(reinterpret_cast<char *>(nbuf1), "+++", 3) != 0)
{
int64_t number = mux_atoi64(nbuf1);
pBuffer = reinterpret_cast<UTF8 *>(getstring_noalloc(fp, true, &nBuffer));
new_mail_message(pBuffer, number);
p = reinterpret_cast<UTF8 *>(fgets(reinterpret_cast<char *>(nbuf1), sizeof(nbuf1), fp));
}
p = reinterpret_cast<UTF8 *>(fgets(reinterpret_cast<char *>(nbuf1), sizeof(nbuf1), fp));
if ( nullptr != p
&& strcmp(reinterpret_cast<char *>(nbuf1), "*** Begin MALIAS ***\n") == 0)
{
malias_read(fp, false);
}
else
{
Log.WriteString(T("ERROR: Couldnt 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<UTF8 *>(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<char *>(getstring_noalloc(fp, true, &nBufferLatin1));
pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode);
m.tolist.assign(reinterpret_cast<const char *>(pBufferUnicode), nBufferUnicode);
pBufferLatin1 = reinterpret_cast<char *>(getstring_noalloc(fp, true, &nBufferLatin1));
pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode);
m.time.assign(reinterpret_cast<const char *>(pBufferUnicode), nBufferUnicode);
pBufferLatin1 = reinterpret_cast<char *>(getstring_noalloc(fp, true, &nBufferLatin1));
pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode);
m.subject.assign(reinterpret_cast<const char *>(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<UTF8 *>(nbuf1);
int64_t number = mux_atoi64(pBufferUnicode);
pBufferLatin1 = reinterpret_cast<char *>(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: Couldnt 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<std::unique_ptr<malias_t>> tmp;
tmp.reserve(count);
for (int i = 0; i < count; i++)
{
// Format is: "%d %d\n", &(m->owner), &(m->numrecep)
//
if (!fgets(reinterpret_cast<char *>(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<malias_t>();
UTF8 *p = reinterpret_cast<UTF8 *>(strchr(reinterpret_cast<char *>(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:<name>\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<char *>(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<const char *>(pMailAlias), nMailAlias);
}
else
{
m->name = "Invalid";
}
// The format of the description is "D:<description>\n"
//
nLen = GetLineTrunc(buffer, LBUF_SIZE, fp);
if (bConvert)
{
size_t nBufferUnicode;
pBufferUnicode = ConvertToUTF8(reinterpret_cast<char *>(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<const char *>(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<char *>(buffer), sizeof(buffer), fp)
&& strcmp(reinterpret_cast<char *>(buffer), "*** Begin MALIAS ***\n") == 0)
{
malias_read(fp, bConvert);
}
else
{
Log.WriteString(T("ERROR: Couldnt find Begin MALIAS." ENDLINE));
return;
}
}
void load_mail(FILE *fp)
{
UTF8 nbuf1[8];
// Read the version number.
//
if (!fgets(reinterpret_cast<char *>(nbuf1), sizeof(nbuf1), fp))
{
return;
}
if (strncmp(reinterpret_cast<char *>(nbuf1), "+V6", 3) == 0)
{
// Started v6 on 2007-MAR-13.
//
load_mail_V6(fp);
}
else if (strncmp(reinterpret_cast<char *>(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 cant 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<int>(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<malias_t>();
// 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<const char *>(pValidMailAlias), nValidMailAlias);
pt->owner = player;
pt->desc.assign(reinterpret_cast<const char *>(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<int>(m->list.size()) - 1; i > -1; i--)
{
const UTF8 *p = Moniker(m->list[i]);
if (strchr(reinterpret_cast<const char *>(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<malias_t*> 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<const UTF8 *>(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<const UTF8 *>(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 dont 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<const char *>(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<const char *>(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<int>(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<const UTF8 *>(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<const UTF8 *>(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<int>(malias.size())));
raw_notify(player, tprintf(M_("MAIL: Vector capacity: %d"),
static_cast<int>(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<int>(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<int>(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<UTF8 *>(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<const char *>(tolist ? tolist : reinterpret_cast<const UTF8 *>("")));
m.time.assign(reinterpret_cast<const char *>(time_str ? time_str : reinterpret_cast<const UTF8 *>("")));
m.subject.assign(reinterpret_cast<const char *>(subject ? subject : reinterpret_cast<const UTF8 *>("")));
m.read = read_flags;
m.sqlite_id = rowid;
}))
{
clear_runtime_mail_data();
mudstate.bSQLiteLoading = false;
return -1;
}
// Load mail aliases.
//
std::vector<std::unique_ptr<malias_t>> 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<malias_t>();
m->owner = owner;
m->name = name ? reinterpret_cast<const char *>(name) : "";
m->desc = desc ? reinterpret_cast<const char *>(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;
}