mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
CResultsSet constructor and NextField require length headers and payloads to fit the result blob. CreateChannel rejects names longer than MAX_CHANNEL_LEN before duplicate checks. Mail aliases cap at 50 per owner and 100 members, including on load.
6562 lines
180 KiB
C++
6562 lines
180 KiB
C++
/*! \file mail_mod.cpp
|
||
* \brief Mail Module — @mail system as a loadable module
|
||
*
|
||
* This module implements the MUX @mail system as a dynamically loaded
|
||
* module. It hooks into server events for player connect/disconnect
|
||
* and provides mux_IMailControl for command dispatch.
|
||
*
|
||
* Core dependencies are accessed exclusively through COM interfaces:
|
||
* mux_INotify — player notification
|
||
* mux_IObjectInfo — object property queries
|
||
* mux_IAttributeAccess — attribute read/write
|
||
* mux_IPermissions — permission checks
|
||
* mux_ILog — logging
|
||
*
|
||
* Mail data is loaded from and persisted to the game's SQLite database
|
||
* via the mux_IMailStorage COM interface provided by the engine.
|
||
*/
|
||
|
||
#include "copyright.h"
|
||
#include "autoconf.h"
|
||
#include "config.h"
|
||
#include "libmux.h"
|
||
#include "modules.h"
|
||
#include "mail_mod.h"
|
||
#include "mux_nls.h"
|
||
#include "mux_format.h"
|
||
#include "mathutil.h"
|
||
#include "color_ops.h"
|
||
#include "mux_table.h"
|
||
|
||
#include <algorithm>
|
||
#include <atomic>
|
||
#include <cstdarg>
|
||
#include <cstring>
|
||
#include <cstdlib>
|
||
#include <cctype>
|
||
|
||
// Windows compatibility for POSIX functions.
|
||
//
|
||
#ifdef _MSC_VER
|
||
#define strcasecmp _stricmp
|
||
#define strtok_r strtok_s
|
||
#endif
|
||
|
||
// Column layout: libmux mux_table_* on co_copy_field (#1667 Phase 3).
|
||
//
|
||
#define append_ljust_field mux_table_append_ljust
|
||
#define append_trunc_field mux_table_append_trunc
|
||
#define append_bytes mux_table_append_bytes
|
||
#define pos_after mux_table_pos_after
|
||
|
||
// mux_sprintf is void; call sites that need the written length (bp += …,
|
||
// truncation checks against remain) go through this. Returns bytes written
|
||
// excluding the trailing NUL — the same contract as mux_vsnprintf.
|
||
//
|
||
static size_t mail_sprintf(UTF8 *buf, size_t nBuf, const UTF8 *fmt, ...)
|
||
{
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
size_t n = mux_vsnprintf(buf, nBuf, fmt, ap);
|
||
va_end(ap);
|
||
return n;
|
||
}
|
||
|
||
// Folder list / review summary line (#1667 Phase 4 B3):
|
||
// [flags] iii (ssss) 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(
|
||
UTF8 *line, size_t nLine,
|
||
const UTF8 *status, int i, size_t nSize,
|
||
const UTF8 *from, const UTF8 *subject)
|
||
{
|
||
size_t pos = 0;
|
||
pos = append_bytes(line, nLine, pos, "[");
|
||
pos = append_bytes(line, nLine, pos,
|
||
reinterpret_cast<const char *>(status));
|
||
pos = append_bytes(line, nLine, pos, "] ");
|
||
if (pos < nLine)
|
||
{
|
||
mux_sprintf(line + pos, nLine - pos, T("%-3d (%4zu) From: "),
|
||
i, nSize);
|
||
pos = pos_after(line, nLine, pos);
|
||
}
|
||
pos = append_ljust_field(line, nLine, pos, from, kMailListFromCols);
|
||
pos = append_bytes(line, nLine, pos, " Sub: ");
|
||
append_trunc_field(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 = append_bytes(line, nLine, pos, "[");
|
||
pos = append_bytes(line, nLine, pos,
|
||
reinterpret_cast<const char *>(status));
|
||
pos = append_bytes(line, nLine, pos, "] ");
|
||
if (pos < nLine)
|
||
{
|
||
mux_sprintf(line + pos, nLine - pos, T("%-3d (%4zu) From: "),
|
||
i, nSize);
|
||
pos = pos_after(line, nLine, pos);
|
||
}
|
||
pos = append_ljust_field(line, nLine, pos, from, kMailListFromCols);
|
||
pos = append_bytes(line, nLine, pos, " At: ");
|
||
pos = append_bytes(line, nLine, pos,
|
||
reinterpret_cast<const char *>(
|
||
(nullptr != time_str) ? time_str : T("")));
|
||
pos = append_bytes(line, nLine, pos, " ");
|
||
pos = append_bytes(line, nLine, pos,
|
||
reinterpret_cast<const char *>(
|
||
(nullptr != conn_tag) ? conn_tag : T("")));
|
||
}
|
||
|
||
// Read / review detail header. subject_cols 0 means no visual truncate
|
||
// (full subject, still buffer-capped by co_copy_field / mux assembly).
|
||
//
|
||
static void format_mail_detail_header(
|
||
UTF8 *hdr, size_t nHdr,
|
||
int i, const UTF8 *from, const UTF8 *time_str, bool bConn,
|
||
int folder,
|
||
const UTF8 *status_lead,
|
||
const char *extra1, const char *extra2, const char *extra3,
|
||
const char *extra4, const char *extra5, const char *extra6,
|
||
const UTF8 *subject, size_t subject_cols)
|
||
{
|
||
size_t pos = 0;
|
||
if (pos < nHdr)
|
||
{
|
||
mux_sprintf(hdr + pos, nHdr - pos, T("%-3d From: "), i);
|
||
pos = pos_after(hdr, nHdr, pos);
|
||
}
|
||
pos = append_ljust_field(hdr, nHdr, pos, from, 16);
|
||
pos = append_bytes(hdr, nHdr, pos, " At: ");
|
||
pos = append_ljust_field(hdr, nHdr, pos, time_str, 25);
|
||
// Format was "At: %-25s %s" with %s = " (Conn)" / " ": two
|
||
// literal spaces between the padded date and the status arg. Dropping
|
||
// them shortens the gap from 4 columns to 2 and fails conformance.
|
||
//
|
||
pos = append_bytes(hdr, nHdr, pos, " ");
|
||
pos = append_bytes(hdr, nHdr, pos, bConn ? " (Conn)" : " ");
|
||
pos = append_bytes(hdr, nHdr, pos, "\r\n");
|
||
if (pos < nHdr)
|
||
{
|
||
mux_sprintf(hdr + pos, nHdr - pos,
|
||
T("Fldr : %-2d Status: %s%s%s%s%s%s%s\r\nSubject: "),
|
||
folder,
|
||
status_lead,
|
||
(nullptr != extra1) ? extra1 : "",
|
||
(nullptr != extra2) ? extra2 : "",
|
||
(nullptr != extra3) ? extra3 : "",
|
||
(nullptr != extra4) ? extra4 : "",
|
||
(nullptr != extra5) ? extra5 : "",
|
||
(nullptr != extra6) ? extra6 : "");
|
||
pos = pos_after(hdr, nHdr, pos);
|
||
}
|
||
if (0 == subject_cols)
|
||
{
|
||
// Full subject into the remainder; codepoint-safe via mux_sprintf.
|
||
//
|
||
if (pos < nHdr)
|
||
{
|
||
mux_sprintf(hdr + pos, nHdr - pos, T("%s"),
|
||
(nullptr != subject) ? subject : T(""));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
append_trunc_field(hdr, nHdr, pos, subject, subject_cols);
|
||
}
|
||
}
|
||
|
||
// "To: name" banner used by @mail/review.
|
||
//
|
||
static void format_mail_to_banner(UTF8 *hdr, size_t nHdr, const UTF8 *name)
|
||
{
|
||
size_t pos = 0;
|
||
pos = append_bytes(hdr, nHdr, pos,
|
||
"–––––"
|
||
"–––––"
|
||
"–––––"
|
||
"–––––"
|
||
"–––"
|
||
" To: ");
|
||
pos = append_ljust_field(hdr, nHdr, pos, name, 25);
|
||
append_bytes(hdr, nHdr, pos,
|
||
" "
|
||
"–––––"
|
||
"–––––"
|
||
"–––––"
|
||
"–––––"
|
||
"–––");
|
||
}
|
||
|
||
// En-dash line for mail display borders.
|
||
//
|
||
static const UTF8 *MOD_DASH_LINE =
|
||
T("––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"–––");
|
||
|
||
// Module bookkeeping.
|
||
//
|
||
static std::atomic<uint32_t> g_cComponents{0};
|
||
static std::atomic<uint32_t> g_cServerLocks{0};
|
||
|
||
// Module entry points.
|
||
//
|
||
static MUX_CLASS_INFO mail_classes[] =
|
||
{
|
||
{ CID_MailMod }
|
||
};
|
||
#define NUM_CLASSES (sizeof(mail_classes)/sizeof(mail_classes[0]))
|
||
|
||
// Required by libmux ModuleLoad — without this, bLoaded stays false and
|
||
// CreateInstance never finds the class (#1191 root cause).
|
||
//
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_CanUnloadNow(void)
|
||
{
|
||
if ( 0 == g_cComponents
|
||
&& 0 == g_cServerLocks)
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
return MUX_S_FALSE;
|
||
}
|
||
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Register(void)
|
||
{
|
||
MUX_RESULT mr = MUX_E_UNEXPECTED;
|
||
|
||
if ( 0 == g_cComponents
|
||
&& 0 == g_cServerLocks)
|
||
{
|
||
mr = mux_RegisterClassObjects(NUM_CLASSES, mail_classes, nullptr);
|
||
}
|
||
return mr;
|
||
}
|
||
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Unregister(void)
|
||
{
|
||
return mux_RevokeClassObjects(NUM_CLASSES, mail_classes);
|
||
}
|
||
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_GetClassObject(MUX_CID cid, MUX_IID iid, void **ppv)
|
||
{
|
||
MUX_RESULT mr = MUX_E_CLASSNOTAVAILABLE;
|
||
|
||
if (CID_MailMod == cid)
|
||
{
|
||
CMailModFactory *pFactory = nullptr;
|
||
try
|
||
{
|
||
pFactory = new CMailModFactory;
|
||
}
|
||
catch (...)
|
||
{
|
||
; // Nothing.
|
||
}
|
||
|
||
if (nullptr == pFactory)
|
||
{
|
||
return MUX_E_OUTOFMEMORY;
|
||
}
|
||
|
||
mr = pFactory->QueryInterface(iid, ppv);
|
||
pFactory->Release();
|
||
}
|
||
return mr;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CMailMod — main module class.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
CMailMod::CMailMod(void) : m_cRef(1),
|
||
m_pILog(nullptr),
|
||
m_pIServerEventsControl(nullptr),
|
||
m_pINotify(nullptr),
|
||
m_pIObjectInfo(nullptr),
|
||
m_pIAttributeAccess(nullptr),
|
||
m_pIPermissions(nullptr),
|
||
m_pIGameConfig(nullptr),
|
||
m_pIMailDelivery(nullptr),
|
||
m_pIStorage(nullptr),
|
||
m_revision(0),
|
||
m_mail_expiration(14),
|
||
m_mail_per_player(250),
|
||
m_bLoading(false)
|
||
{
|
||
g_cComponents++;
|
||
}
|
||
|
||
MUX_RESULT CMailMod::FinalConstruct(void)
|
||
{
|
||
MUX_RESULT mr;
|
||
|
||
// Acquire logging interface.
|
||
//
|
||
mr = mux_CreateInstance(CID_Log, nullptr, UseSameProcess,
|
||
IID_ILog,
|
||
reinterpret_cast<void **>(&m_pILog));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
// Register for server events.
|
||
//
|
||
mux_IServerEventsSink *pIServerEventsSink = nullptr;
|
||
mr = QueryInterface(IID_IServerEventsSink,
|
||
reinterpret_cast<void **>(&pIServerEventsSink));
|
||
if (MUX_SUCCEEDED(mr))
|
||
{
|
||
mr = mux_CreateInstance(CID_ServerEventsSource, nullptr,
|
||
UseSameProcess, IID_IServerEventsControl,
|
||
reinterpret_cast<void **>(&m_pIServerEventsControl));
|
||
if (MUX_SUCCEEDED(mr))
|
||
{
|
||
m_pIServerEventsControl->Advise(pIServerEventsSink);
|
||
}
|
||
pIServerEventsSink->Release();
|
||
}
|
||
|
||
// Acquire core interfaces.
|
||
//
|
||
mr = mux_CreateInstance(CID_Notify, nullptr, UseSameProcess,
|
||
IID_INotify,
|
||
reinterpret_cast<void **>(&m_pINotify));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_ObjectInfo, nullptr, UseSameProcess,
|
||
IID_IObjectInfo,
|
||
reinterpret_cast<void **>(&m_pIObjectInfo));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_AttributeAccess, nullptr, UseSameProcess,
|
||
IID_IAttributeAccess,
|
||
reinterpret_cast<void **>(&m_pIAttributeAccess));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_Permissions, nullptr, UseSameProcess,
|
||
IID_IPermissions,
|
||
reinterpret_cast<void **>(&m_pIPermissions));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
// Optional (#1654): an engine without CID_GameConfig lacks the class,
|
||
// and the one consumer (the @mail/stats charge) then keeps the old
|
||
// behaviour of not charging. Hard-failing would make the module
|
||
// unloadable against a binary it otherwise works with.
|
||
//
|
||
mr = mux_CreateInstance(CID_GameConfig, nullptr, UseSameProcess,
|
||
IID_IGameConfig,
|
||
reinterpret_cast<void **>(&m_pIGameConfig));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
m_pIGameConfig = nullptr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_MailDelivery, nullptr, UseSameProcess,
|
||
IID_IMailDelivery,
|
||
reinterpret_cast<void **>(&m_pIMailDelivery));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
// Log that we are alive.
|
||
//
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr2 = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("INI"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr2) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module loaded."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
|
||
return mr;
|
||
}
|
||
|
||
CMailMod::~CMailMod()
|
||
{
|
||
// Release storage interface.
|
||
//
|
||
if (nullptr != m_pIStorage)
|
||
{
|
||
m_pIStorage->Release();
|
||
m_pIStorage = nullptr;
|
||
}
|
||
|
||
// Free mail data.
|
||
//
|
||
ClearRuntimeData();
|
||
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("INI"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module unloading."));
|
||
m_pILog->end_log();
|
||
}
|
||
m_pILog->Release();
|
||
m_pILog = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIServerEventsControl)
|
||
{
|
||
m_pIServerEventsControl->Release();
|
||
m_pIServerEventsControl = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->Release();
|
||
m_pINotify = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->Release();
|
||
m_pIObjectInfo = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIAttributeAccess)
|
||
{
|
||
m_pIAttributeAccess->Release();
|
||
m_pIAttributeAccess = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->Release();
|
||
m_pIPermissions = nullptr;
|
||
}
|
||
if (nullptr != m_pIGameConfig)
|
||
{
|
||
m_pIGameConfig->Release();
|
||
}
|
||
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->Release();
|
||
m_pIMailDelivery = nullptr;
|
||
}
|
||
|
||
g_cComponents--;
|
||
}
|
||
|
||
void CMailMod::ClearRuntimeData(void)
|
||
{
|
||
m_mail_htab.clear();
|
||
m_mail_list.clear();
|
||
m_malias.clear();
|
||
}
|
||
|
||
MUX_RESULT CMailMod::QueryInterface(MUX_IID iid, void **ppv)
|
||
{
|
||
if (mux_IID_IUnknown == iid)
|
||
{
|
||
*ppv = static_cast<mux_IMailControl *>(this);
|
||
}
|
||
else if (IID_IMailControl == iid)
|
||
{
|
||
*ppv = static_cast<mux_IMailControl *>(this);
|
||
}
|
||
else if (IID_IServerEventsSink == iid)
|
||
{
|
||
*ppv = static_cast<mux_IServerEventsSink *>(this);
|
||
}
|
||
else
|
||
{
|
||
*ppv = nullptr;
|
||
return MUX_E_NOINTERFACE;
|
||
}
|
||
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
uint32_t CMailMod::AddRef(void)
|
||
{
|
||
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
|
||
}
|
||
|
||
uint32_t CMailMod::Release(void)
|
||
{
|
||
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
|
||
if (1 == prev)
|
||
{
|
||
delete this;
|
||
return 0;
|
||
}
|
||
return prev - 1;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Message body management.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Largest message index we will honor (#1065 / engine #843 parity).
|
||
// Message numbers can come from a corrupt mail database; an absurd value
|
||
// would drive a huge resize or a wild index.
|
||
//
|
||
static constexpr int MAIL_DB_LIMIT = 0x04000000; // 67,108,864
|
||
|
||
void CMailMod::mail_db_grow(int newtop)
|
||
{
|
||
if (newtop <= static_cast<int>(m_mail_list.size()))
|
||
{
|
||
return;
|
||
}
|
||
if (newtop > MAIL_DB_LIMIT)
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_BUGS, T("BUG"), T("MAIL"));
|
||
if (fStarted)
|
||
{
|
||
UTF8 buf[128];
|
||
mux_sprintf(buf, sizeof(buf), T("mail_db_grow: refusing absurd mail size %d."), newtop);
|
||
m_pILog->log_text(buf);
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
m_mail_list.resize(newtop);
|
||
}
|
||
|
||
int CMailMod::new_mail_message(const UTF8 *message, int number)
|
||
{
|
||
bool bTruncated = false;
|
||
size_t nLen = strlen(reinterpret_cast<const char *>(message));
|
||
if (nLen > MOD_LBUF_SIZE - 1)
|
||
{
|
||
bTruncated = true;
|
||
nLen = MOD_LBUF_SIZE - 1;
|
||
}
|
||
|
||
// Allocating a new body, or loading an existing one back from storage?
|
||
// The engine splits these into two functions -- MessageAdd() allocates
|
||
// and takes a reference, new_mail_message() loads and does not -- and
|
||
// this one does both, so the distinction has to be captured here (#1587).
|
||
//
|
||
const bool bAllocating = (NOTHING == number);
|
||
|
||
// If number is NOTHING (-1), find a free slot.
|
||
//
|
||
if (NOTHING == number)
|
||
{
|
||
int top = static_cast<int>(m_mail_list.size());
|
||
for (int i = 0; i < top; i++)
|
||
{
|
||
if (m_mail_list[i].m_nRefs <= 0 && m_mail_list[i].m_pMessage.empty())
|
||
{
|
||
number = i;
|
||
break;
|
||
}
|
||
}
|
||
if (NOTHING == number)
|
||
{
|
||
number = top;
|
||
}
|
||
}
|
||
|
||
// Reject negative / absurd indices before grow (#1065).
|
||
//
|
||
if (number < 0 || number > MAIL_DB_LIMIT)
|
||
{
|
||
return NOTHING;
|
||
}
|
||
|
||
mail_db_grow(number + 1);
|
||
|
||
// Grow may have refused an absurd size; do not index past the vector.
|
||
//
|
||
if (number >= static_cast<int>(m_mail_list.size()))
|
||
{
|
||
return NOTHING;
|
||
}
|
||
|
||
struct mail_body &pm = m_mail_list[number];
|
||
pm.m_pMessage.assign(reinterpret_cast<const char *>(message), nLen);
|
||
|
||
// Take the creation reference (#1587). The engine does this in
|
||
// MessageAdd and says so above it: "This function returns a reference to
|
||
// the message and the reference count is increased to reflect that."
|
||
// Without it the arithmetic on a send is 0 -> Inc per recipient -> Dec
|
||
// once at the end of mail_to_list, so a single-recipient message returns
|
||
// to zero the instant it is sent and the body is freed. Two recipients
|
||
// survived undercounted by one, and died when the first of them cleared
|
||
// their copy.
|
||
//
|
||
// Only when allocating. On load the per-header MessageReferenceInc in
|
||
// the storage callback already counts every reference there is, and
|
||
// taking another here would leak every body for the life of the process.
|
||
//
|
||
if (bAllocating)
|
||
{
|
||
pm.m_nRefs = 0;
|
||
MessageReferenceInc(number);
|
||
}
|
||
|
||
// #1192: Persist body + mail_db_top on live creates. Skipped while
|
||
// m_bLoading so SQLite load does not write back through itself.
|
||
// Also feeds the #1191 softcode gen-sync reload.
|
||
//
|
||
sqlite_wt_mail_body(number,
|
||
reinterpret_cast<const UTF8 *>(pm.m_pMessage.c_str()));
|
||
|
||
if (bTruncated && nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_BUGS, T("BUG"), T("MAIL"));
|
||
if (fStarted)
|
||
{
|
||
UTF8 buf[128];
|
||
mux_sprintf(buf, sizeof(buf), T("new_mail_message: Mail message %d truncated."), number);
|
||
m_pILog->log_text(buf);
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
return number;
|
||
}
|
||
|
||
const UTF8 *CMailMod::MessageFetch(int number)
|
||
{
|
||
if (number < 0 || number >= static_cast<int>(m_mail_list.size()))
|
||
{
|
||
return M_("MAIL: This mail message does not exist in the database. Please alert your admin.");
|
||
}
|
||
if (!m_mail_list[number].m_pMessage.empty())
|
||
{
|
||
return reinterpret_cast<const UTF8 *>(m_mail_list[number].m_pMessage.c_str());
|
||
}
|
||
return M_("MAIL: This mail message does not exist in the database. Please alert your admin.");
|
||
}
|
||
|
||
size_t CMailMod::MessageFetchSize(int number)
|
||
{
|
||
if (number < 0 || number >= static_cast<int>(m_mail_list.size()))
|
||
{
|
||
return 0;
|
||
}
|
||
return m_mail_list[number].m_pMessage.size();
|
||
}
|
||
|
||
void CMailMod::MessageReferenceInc(int number)
|
||
{
|
||
if (0 <= number && number < static_cast<int>(m_mail_list.size()))
|
||
{
|
||
m_mail_list[number].m_nRefs++;
|
||
}
|
||
}
|
||
|
||
void CMailMod::MessageReferenceDec(int number)
|
||
{
|
||
if (number < 0 || number >= static_cast<int>(m_mail_list.size()))
|
||
{
|
||
return;
|
||
}
|
||
|
||
m_mail_list[number].m_nRefs--;
|
||
if (m_mail_list[number].m_nRefs <= 0)
|
||
{
|
||
sqlite_wt_delete_mail_body(number);
|
||
m_mail_list[number].m_pMessage.clear();
|
||
m_mail_list[number].m_nRefs = 0;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Mail list management — per-player std::list<mail>.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
std::list<mail> *CMailMod::MailList(dbref player)
|
||
{
|
||
auto it = m_mail_htab.find(player);
|
||
if (it != m_mail_htab.end() && !it->second.empty())
|
||
{
|
||
return &it->second;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
void CMailMod::MailListRemoveAll(dbref player)
|
||
{
|
||
auto it = m_mail_htab.find(player);
|
||
if (it == m_mail_htab.end())
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (auto &m : it->second)
|
||
{
|
||
MessageReferenceDec(m.number);
|
||
}
|
||
m_mail_htab.erase(it);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// SQLite write-through helpers.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::bump_revision(void)
|
||
{
|
||
m_revision++;
|
||
if (m_revision < 0)
|
||
{
|
||
m_revision = 1;
|
||
}
|
||
}
|
||
|
||
// Report a refused storage write (#1630). Silent on success, so it can wrap
|
||
// a call directly: log_storage_failure(m_pIStorage->Foo(k), "Foo(k=%d)", k);
|
||
//
|
||
// The message carries the operation, the key that identifies the row, and the
|
||
// result code, because "a write failed" without the key is not actionable --
|
||
// #1587 needed to know *which* body could not be deleted.
|
||
//
|
||
void CMailMod::log_storage_failure(MUX_RESULT mr, const char *fmt, ...)
|
||
{
|
||
if ( MUX_SUCCEEDED(mr)
|
||
|| nullptr == m_pILog)
|
||
{
|
||
return;
|
||
}
|
||
|
||
char detail[192];
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
mux_vsnprintf(reinterpret_cast<UTF8 *>(detail), sizeof(detail),
|
||
reinterpret_cast<const UTF8 *>(fmt), ap);
|
||
va_end(ap);
|
||
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("MAI"), T("DB"));
|
||
if (fStarted)
|
||
{
|
||
UTF8 buf[256];
|
||
mux_sprintf(buf, sizeof(buf), T("Mail module: storage write refused: %s; result %d."),
|
||
detail, mr);
|
||
m_pILog->log_text(buf);
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
|
||
void CMailMod::sqlite_wt_insert_mail(struct mail *mp)
|
||
{
|
||
if (m_bLoading || nullptr == m_pIStorage) return;
|
||
|
||
int64_t rowid = -1;
|
||
MUX_RESULT mr = m_pIStorage->InsertMailHeader(
|
||
mp->to, mp->from, mp->number,
|
||
reinterpret_cast<const UTF8 *>(mp->tolist.c_str()),
|
||
reinterpret_cast<const UTF8 *>(mp->time.c_str()),
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()),
|
||
mp->read, &rowid);
|
||
|
||
// The result was already consumed for the rowid, but a failure here
|
||
// means the message is in memory and not in the database -- it survives
|
||
// the session and vanishes on restart. Worth saying so (#1630).
|
||
//
|
||
log_storage_failure(mr, "InsertMailHeader(to=#%d, from=#%d, number=%d)",
|
||
static_cast<int>(mp->to), static_cast<int>(mp->from), mp->number);
|
||
|
||
mp->sqlite_id = MUX_SUCCEEDED(mr) ? rowid : -1;
|
||
bump_revision();
|
||
}
|
||
|
||
void CMailMod::sqlite_wt_update_mail_flags(struct mail *mp)
|
||
{
|
||
if (m_bLoading || nullptr == m_pIStorage || mp->sqlite_id < 0) return;
|
||
|
||
log_storage_failure(
|
||
m_pIStorage->UpdateMailReadFlags(mp->sqlite_id, mp->read),
|
||
"UpdateMailReadFlags(id=%lld, read=%d)",
|
||
static_cast<long long>(mp->sqlite_id), mp->read);
|
||
bump_revision();
|
||
}
|
||
|
||
void CMailMod::sqlite_wt_delete_mail(struct mail *mp)
|
||
{
|
||
if (m_bLoading || nullptr == m_pIStorage || mp->sqlite_id < 0) return;
|
||
|
||
log_storage_failure(m_pIStorage->DeleteMailHeader(mp->sqlite_id),
|
||
"DeleteMailHeader(id=%lld)",
|
||
static_cast<long long>(mp->sqlite_id));
|
||
bump_revision();
|
||
}
|
||
|
||
void CMailMod::sqlite_wt_delete_all_mail(int to_player)
|
||
{
|
||
if (m_bLoading || nullptr == m_pIStorage) return;
|
||
|
||
log_storage_failure(m_pIStorage->DeleteAllMailHeaders(to_player),
|
||
"DeleteAllMailHeaders(to=#%d)", to_player);
|
||
bump_revision();
|
||
}
|
||
|
||
void CMailMod::sqlite_wt_mail_body(int number, const UTF8 *message)
|
||
{
|
||
if (m_bLoading || nullptr == m_pIStorage) return;
|
||
|
||
log_storage_failure(m_pIStorage->SyncMailBody(number, message),
|
||
"SyncMailBody(number=%d)", number);
|
||
|
||
// Keep the loader gate current (#783 / engine MessageAdd parity):
|
||
// sqlite_load_mail / module Initialize only size bodies from
|
||
// mail_db_top meta. Without this, in-game module mail is lost on reboot.
|
||
//
|
||
log_storage_failure(m_pIStorage->PutMeta(T("mail_db_top"),
|
||
static_cast<int>(m_mail_list.size())),
|
||
"PutMeta(mail_db_top=%d)", static_cast<int>(m_mail_list.size()));
|
||
bump_revision();
|
||
}
|
||
|
||
void CMailMod::sqlite_wt_delete_mail_body(int number)
|
||
{
|
||
if (m_bLoading || nullptr == m_pIStorage) return;
|
||
|
||
// Check the result (#1587). mail_headers.body_number REFERENCES
|
||
// mail_bodies(number), so deleting a body a header still points at is
|
||
// refused with a foreign-key violation -- and this discarded that,
|
||
// which is the only reason the refcount bug above did not destroy mail
|
||
// outright. A delete that cannot happen is a fact worth knowing rather
|
||
// than an accident worth relying on. The engine's equivalent logs it.
|
||
//
|
||
if (MUX_FAILED(m_pIStorage->DeleteMailBody(number)) && nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("MAI"), T("BODY"));
|
||
if (fStarted)
|
||
{
|
||
UTF8 buf[160];
|
||
mux_sprintf(buf, sizeof(buf), T("Mail module: body %d could not be deleted; a header still "
|
||
"references it."), number);
|
||
m_pILog->log_text(buf);
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
bump_revision();
|
||
}
|
||
|
||
void CMailMod::sqlite_wt_sync_all_aliases(void)
|
||
{
|
||
if (m_bLoading || nullptr == m_pIStorage) return;
|
||
|
||
// Clear existing aliases.
|
||
//
|
||
log_storage_failure(m_pIStorage->ClearMailAliases(),
|
||
"ClearMailAliases()");
|
||
|
||
for (size_t i = 0; i < m_malias.size(); i++)
|
||
{
|
||
malias_t *m = m_malias[i].get();
|
||
if (nullptr == m) continue;
|
||
|
||
// Build space-separated member list. #1197: clamp snprintf return
|
||
// (it can exceed remain) and refuse to write a separator when the
|
||
// buffer is nearly full — same class as #1066 make_numlist.
|
||
//
|
||
char members_buf[MOD_LBUF_SIZE];
|
||
char *bp = members_buf;
|
||
size_t remain = sizeof(members_buf);
|
||
for (size_t j = 0; j < m->list.size() && remain > 1; j++)
|
||
{
|
||
if (j > 0)
|
||
{
|
||
if (remain <= 1)
|
||
{
|
||
break;
|
||
}
|
||
*bp++ = ' ';
|
||
remain--;
|
||
}
|
||
size_t n = mail_sprintf(reinterpret_cast<UTF8 *>(bp), remain,
|
||
T("%d"), m->list[j]);
|
||
if (n + 1 >= remain)
|
||
{
|
||
// Truncated — leave the partial write and stop.
|
||
//
|
||
bp += remain - 1;
|
||
remain = 1;
|
||
break;
|
||
}
|
||
bp += n;
|
||
remain -= n;
|
||
}
|
||
*bp = '\0';
|
||
|
||
log_storage_failure(m_pIStorage->SyncMailAlias(
|
||
m->owner,
|
||
reinterpret_cast<const UTF8 *>(m->name.c_str()),
|
||
reinterpret_cast<const UTF8 *>(m->desc.c_str()),
|
||
static_cast<int>(m->desc_width),
|
||
reinterpret_cast<const UTF8 *>(members_buf)),
|
||
"SyncMailAlias(owner=#%d, name=%s)",
|
||
static_cast<int>(m->owner), m->name.c_str());
|
||
}
|
||
bump_revision();
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Data loading from SQLite.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
bool CMailMod::LoadMailBodies(void)
|
||
{
|
||
if (nullptr == m_pIStorage) return false;
|
||
|
||
MUX_RESULT mr = m_pIStorage->LoadAllMailBodies(
|
||
[](void *ctx, int number, const UTF8 *message)
|
||
{
|
||
CMailMod *pThis = static_cast<CMailMod *>(ctx);
|
||
if (nullptr != message)
|
||
{
|
||
pThis->new_mail_message(message, number);
|
||
}
|
||
}, this);
|
||
|
||
return MUX_SUCCEEDED(mr);
|
||
}
|
||
|
||
bool CMailMod::LoadMailHeaders(void)
|
||
{
|
||
if (nullptr == m_pIStorage) return false;
|
||
|
||
MUX_RESULT mr = m_pIStorage->LoadAllMailHeaders(
|
||
[](void *ctx, 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)
|
||
{
|
||
CMailMod *pThis = static_cast<CMailMod *>(ctx);
|
||
|
||
mail m;
|
||
m.sqlite_id = rowid;
|
||
m.to = to_player;
|
||
m.from = from_player;
|
||
m.number = body_number;
|
||
|
||
pThis->MessageReferenceInc(m.number);
|
||
|
||
m.tolist = tolist ? reinterpret_cast<const char *>(tolist) : "";
|
||
m.time = time_str ? reinterpret_cast<const char *>(time_str) : "";
|
||
m.subject = subject ? reinterpret_cast<const char *>(subject) : "";
|
||
m.read = read_flags;
|
||
|
||
pThis->m_mail_htab[to_player].push_back(std::move(m));
|
||
}, this);
|
||
|
||
return MUX_SUCCEEDED(mr);
|
||
}
|
||
|
||
bool CMailMod::LoadMailAliases(void)
|
||
{
|
||
if (nullptr == m_pIStorage) return false;
|
||
|
||
MUX_RESULT mr = m_pIStorage->LoadAllMailAliases(
|
||
[](void *ctx, int owner, const UTF8 *name, const UTF8 *desc,
|
||
int desc_width, const UTF8 *members)
|
||
{
|
||
CMailMod *pThis = static_cast<CMailMod *>(ctx);
|
||
|
||
auto m = std::make_unique<malias_t>();
|
||
|
||
m->owner = owner;
|
||
|
||
const char *pName = reinterpret_cast<const char *>(name);
|
||
const char *pDesc = reinterpret_cast<const char *>(desc);
|
||
|
||
m->name = pName ? pName : "";
|
||
m->desc = pDesc ? pDesc : "";
|
||
m->desc_width = desc_width;
|
||
|
||
// Parse space-separated member list.
|
||
// #1875: drop excess members beyond MAX_MALIAS_MEMBERS on load.
|
||
//
|
||
m->list.clear();
|
||
const char *pMembers = reinterpret_cast<const char *>(members);
|
||
if (nullptr != pMembers && pMembers[0] != '\0')
|
||
{
|
||
char buf[MOD_LBUF_SIZE];
|
||
strncpy(buf, pMembers, MOD_LBUF_SIZE - 1);
|
||
buf[MOD_LBUF_SIZE - 1] = '\0';
|
||
char *p = buf;
|
||
while (*p)
|
||
{
|
||
while (*p == ' ') p++;
|
||
if (*p == '\0') break;
|
||
if (m->list.size() < MAX_MALIAS_MEMBERS)
|
||
{
|
||
m->list.push_back(mux_atoi64(reinterpret_cast<const UTF8 *>(p)));
|
||
}
|
||
while (*p && *p != ' ') p++;
|
||
}
|
||
}
|
||
|
||
// #1875: skip aliases past the per-owner cap so a hostile DB
|
||
// cannot re-inflate unbounded growth at LoadMailAliases.
|
||
//
|
||
size_t nOwned = 0;
|
||
for (size_t i = 0; i < pThis->m_malias.size(); i++)
|
||
{
|
||
if ( pThis->m_malias[i]
|
||
&& pThis->m_malias[i]->owner == owner)
|
||
{
|
||
nOwned++;
|
||
}
|
||
}
|
||
if (nOwned >= MAX_MALIAS_PER_OWNER)
|
||
{
|
||
return;
|
||
}
|
||
|
||
pThis->m_malias.push_back(std::move(m));
|
||
}, this);
|
||
|
||
return MUX_SUCCEEDED(mr);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Composition attributes — AF_INTERNAL / AF_DARK; must use raw attr APIs.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
bool CMailMod::set_comp_attr(dbref player, int attrnum, const UTF8 *value)
|
||
{
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return false;
|
||
}
|
||
return MUX_SUCCEEDED(m_pIObjectInfo->AtrAddRaw(player, attrnum,
|
||
value ? value : T("")));
|
||
}
|
||
|
||
bool CMailMod::get_comp_attr(dbref player, int attrnum, UTF8 *buf, size_t nBuf)
|
||
{
|
||
if (nullptr == buf || 0 == nBuf)
|
||
{
|
||
return false;
|
||
}
|
||
buf[0] = '\0';
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return false;
|
||
}
|
||
dbref aowner = NOTHING;
|
||
int aflags = 0;
|
||
MUX_RESULT mr = m_pIObjectInfo->AtrGet(player, attrnum, buf, nBuf,
|
||
&aowner, &aflags);
|
||
return MUX_SUCCEEDED(mr);
|
||
}
|
||
|
||
bool CMailMod::clr_comp_attr(dbref player, int attrnum)
|
||
{
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return false;
|
||
}
|
||
return MUX_SUCCEEDED(m_pIObjectInfo->AtrClr(player, attrnum));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// mux_IMailControl implementation.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CMailMod::Initialize(mux_IMailStorage *pStorage,
|
||
int mail_expiration, int mail_per_player)
|
||
{
|
||
if (nullptr == pStorage)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
// Store and AddRef the storage interface.
|
||
//
|
||
m_pIStorage = pStorage;
|
||
m_pIStorage->AddRef();
|
||
|
||
m_mail_expiration = mail_expiration;
|
||
m_mail_per_player = mail_per_player;
|
||
|
||
// Check for mail_db_top metadata to know if mail data exists.
|
||
//
|
||
int mail_top = 0;
|
||
m_pIStorage->GetMeta(T("mail_db_top"), &mail_top);
|
||
|
||
m_bLoading = true;
|
||
|
||
ClearRuntimeData();
|
||
mail_db_grow(mail_top + 1);
|
||
|
||
if (!LoadMailBodies())
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("MAIL"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module: LoadMailBodies failed."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!LoadMailHeaders())
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("MAIL"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module: LoadMailHeaders failed."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!LoadMailAliases())
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("MAIL"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module: LoadMailAliases failed."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
m_bLoading = false;
|
||
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("MAIL"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module: data loaded from SQLite."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Selector parsing and matching — ported from mail.cpp.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
static const UTF8 *mailmsg[] =
|
||
{
|
||
M_("MAIL: Invalid message range"),
|
||
M_("MAIL: Invalid message number"),
|
||
M_("MAIL: Invalid age"),
|
||
M_("MAIL: Invalid dbref #"),
|
||
M_("MAIL: Invalid player"),
|
||
M_("MAIL: Invalid message specification"),
|
||
M_("MAIL: Invalid player or trying to send @mail to a @malias without a subject"),
|
||
};
|
||
|
||
#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
|
||
|
||
bool CMailMod::parse_msglist(const UTF8 *msglist, struct mail_selector *ms,
|
||
dbref player)
|
||
{
|
||
ms->low = 0;
|
||
ms->high = 0;
|
||
ms->flags = 0x0FFF | M_MSUNREAD;
|
||
ms->player = 0;
|
||
ms->days = -1;
|
||
ms->day_comp = 0;
|
||
|
||
if (nullptr == msglist || '\0' == *msglist)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
const UTF8 *p = msglist;
|
||
while (isspace(*p))
|
||
{
|
||
p++;
|
||
}
|
||
|
||
if ('\0' == *p)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (isdigit(*p))
|
||
{
|
||
const char *q = strchr(reinterpret_cast<const char *>(p), '-');
|
||
if (q)
|
||
{
|
||
q++;
|
||
ms->low = mux_atoi64(p);
|
||
if (ms->low <= 0)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_RANGE]);
|
||
return false;
|
||
}
|
||
if ('\0' == *q)
|
||
{
|
||
ms->high = 0;
|
||
}
|
||
else
|
||
{
|
||
ms->high = mux_atoi64(reinterpret_cast<const UTF8 *>(q));
|
||
if (ms->low > ms->high)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_RANGE]);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ms->low = ms->high = mux_atoi64(p);
|
||
if (ms->low <= 0)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_NUMBER]);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
switch (*p)
|
||
{
|
||
case '-':
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_RANGE]);
|
||
return false;
|
||
}
|
||
ms->high = mux_atoi64(p);
|
||
if (ms->high <= 0)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_RANGE]);
|
||
return false;
|
||
}
|
||
break;
|
||
|
||
case '~':
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_AGE]);
|
||
return false;
|
||
}
|
||
ms->day_comp = 0;
|
||
ms->days = mux_atoi64(p);
|
||
if (ms->days < 0)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_AGE]);
|
||
return false;
|
||
}
|
||
break;
|
||
|
||
case '<':
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_AGE]);
|
||
return false;
|
||
}
|
||
ms->day_comp = -1;
|
||
ms->days = mux_atoi64(p);
|
||
if (ms->days < 0)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_AGE]);
|
||
return false;
|
||
}
|
||
break;
|
||
|
||
case '>':
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_AGE]);
|
||
return false;
|
||
}
|
||
ms->day_comp = 1;
|
||
ms->days = mux_atoi64(p);
|
||
if (ms->days < 0)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_AGE]);
|
||
return false;
|
||
}
|
||
break;
|
||
|
||
case '#':
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_DBREF]);
|
||
return false;
|
||
}
|
||
ms->player = static_cast<dbref>(mux_atoi64(p));
|
||
break;
|
||
|
||
case '*':
|
||
// From player name — use IObjectInfo::MatchThing.
|
||
//
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_PLAYER]);
|
||
return false;
|
||
}
|
||
{
|
||
UTF8 namebuf[MOD_LBUF_SIZE];
|
||
namebuf[0] = '*';
|
||
strncpy(reinterpret_cast<char *>(namebuf + 1),
|
||
reinterpret_cast<const char *>(p),
|
||
MOD_LBUF_SIZE - 2);
|
||
namebuf[MOD_LBUF_SIZE - 1] = '\0';
|
||
dbref result = NOTHING;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->MatchThing(player, namebuf, &result);
|
||
}
|
||
if (NOTHING == result)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
mailmsg[MAIL_INVALID_PLAYER_OR_USING_MALIAS]);
|
||
return false;
|
||
}
|
||
ms->player = result;
|
||
}
|
||
break;
|
||
|
||
case 'a':
|
||
case 'A':
|
||
p++;
|
||
if ('\0' == *p || ((*p != 'l' && *p != 'L')))
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_SPEC]);
|
||
return false;
|
||
}
|
||
p++;
|
||
if ('\0' == *p || ((*p != 'l' && *p != 'L')))
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_SPEC]);
|
||
return false;
|
||
}
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
ms->flags = M_ALL;
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_SPEC]);
|
||
return false;
|
||
}
|
||
break;
|
||
|
||
case 'u':
|
||
case 'U':
|
||
p++;
|
||
if ('\0' == *p)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: U is ambiguous (urgent or unread?)"));
|
||
return false;
|
||
}
|
||
if ('r' == *p || 'R' == *p)
|
||
{
|
||
ms->flags = M_URGENT;
|
||
}
|
||
else if ('n' == *p || 'N' == *p)
|
||
{
|
||
ms->flags = M_MSUNREAD;
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_SPEC]);
|
||
return false;
|
||
}
|
||
break;
|
||
|
||
case 'r':
|
||
case 'R':
|
||
ms->flags = M_ISREAD;
|
||
break;
|
||
|
||
case 'c':
|
||
case 'C':
|
||
ms->flags = M_CLEARED;
|
||
break;
|
||
|
||
case 't':
|
||
case 'T':
|
||
ms->flags = M_TAG;
|
||
break;
|
||
|
||
case 'm':
|
||
case 'M':
|
||
ms->flags = M_MASS;
|
||
break;
|
||
|
||
case 'f':
|
||
case 'F':
|
||
ms->flags = M_FORWARD;
|
||
break;
|
||
|
||
default:
|
||
m_pINotify->RawNotify(player, mailmsg[MAIL_INVALID_SPEC]);
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
static int sign(int x)
|
||
{
|
||
if (x == 0) return 0;
|
||
return (x < 0) ? -1 : 1;
|
||
}
|
||
|
||
bool CMailMod::mail_match(struct mail *mp, struct mail_selector &ms, int num)
|
||
{
|
||
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;
|
||
}
|
||
|
||
CLinearTimeAbsolute ltaNow;
|
||
ltaNow.GetLocal();
|
||
|
||
CLinearTimeAbsolute ltaMail;
|
||
if (ltaMail.SetString(reinterpret_cast<const UTF8 *>(mp->time.c_str())))
|
||
{
|
||
CLinearTimeDelta ltd(ltaMail, ltaNow);
|
||
int iDiffDays = ltd.ReturnDays();
|
||
if (sign(iDiffDays - ms.days) == ms.day_comp)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
int CMailMod::player_folder(dbref player)
|
||
{
|
||
if (nullptr == m_pIAttributeAccess)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
UTF8 buf[64];
|
||
size_t nLen = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, player,
|
||
T("Mailcurf"), buf, sizeof(buf) - 1, &nLen);
|
||
if (MUX_FAILED(mr) || 0 == nLen)
|
||
{
|
||
return 0;
|
||
}
|
||
buf[nLen] = '\0';
|
||
return mux_atoi64(buf);
|
||
}
|
||
|
||
void CMailMod::set_player_folder(dbref player, int fnum)
|
||
{
|
||
if (nullptr == m_pIAttributeAccess)
|
||
{
|
||
return;
|
||
}
|
||
UTF8 buf[16];
|
||
mux_sprintf(buf, sizeof(buf), T("%d"), fnum);
|
||
set_attr_checked(player, T("Mailcurf"), buf, T("current folder"));
|
||
}
|
||
|
||
// Write an attribute and say so when the write is refused (#1620).
|
||
//
|
||
// SetAttribute is permission-checked. The engine writes some of the same
|
||
// attributes as GOD with AF_CONST, which bCanSetAttr denies in every branch
|
||
// -- so on a game that has run both implementations these writes can fail,
|
||
// and every call site here used to discard the result. A refused folder
|
||
// write means the player's folder list silently reverts, which looks like
|
||
// the command never ran.
|
||
//
|
||
// This does not make the write land; it makes the loss visible.
|
||
//
|
||
void CMailMod::set_attr_checked(dbref player, const UTF8 *pAttr,
|
||
const UTF8 *pValue, const UTF8 *pWhere)
|
||
{
|
||
MUX_RESULT mr = m_pIAttributeAccess->SetAttribute(1, player, pAttr,
|
||
pValue);
|
||
if (MUX_FAILED(mr) && nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("MAI"), T("ATTR"));
|
||
if (fStarted)
|
||
{
|
||
UTF8 logbuf[256];
|
||
mux_sprintf(logbuf, sizeof(logbuf), T("Mail module: %s write refused (%s on #%d, result %d); "
|
||
"change not saved."),
|
||
reinterpret_cast<const char *>(pWhere),
|
||
reinterpret_cast<const char *>(pAttr),
|
||
static_cast<int>(player), mr);
|
||
m_pILog->log_text(logbuf);
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
const UTF8 *CMailMod::get_folder_name(dbref player, int fld)
|
||
{
|
||
if (nullptr == m_pIAttributeAccess)
|
||
{
|
||
return T("unnamed");
|
||
}
|
||
|
||
UTF8 aFolders[MOD_LBUF_SIZE];
|
||
size_t nFolders = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, player,
|
||
T("Mailfolders"), aFolders, sizeof(aFolders) - 1, &nFolders);
|
||
if (MUX_FAILED(mr) || 0 == nFolders)
|
||
{
|
||
return T("unnamed");
|
||
}
|
||
aFolders[nFolders] = '\0';
|
||
|
||
// Build pattern "N:" where N is the folder number.
|
||
//
|
||
char pattern[16];
|
||
mux_sprintf(reinterpret_cast<UTF8 *>(pattern), sizeof(pattern),
|
||
T("%d:"), fld);
|
||
size_t nPattern = strlen(pattern);
|
||
|
||
const char *found = strstr(reinterpret_cast<const char *>(aFolders), pattern);
|
||
if (nullptr == found)
|
||
{
|
||
return T("unnamed");
|
||
}
|
||
|
||
thread_local UTF8 result[FOLDER_NAME_LEN + 1];
|
||
const char *p = found + nPattern;
|
||
char *q = reinterpret_cast<char *>(result);
|
||
while (*p && *p != ':' && q < reinterpret_cast<char *>(result) + FOLDER_NAME_LEN)
|
||
{
|
||
*q++ = *p++;
|
||
}
|
||
*q = '\0';
|
||
return result;
|
||
}
|
||
|
||
int CMailMod::get_folder_number(dbref player, const UTF8 *name)
|
||
{
|
||
if (nullptr == m_pIAttributeAccess || nullptr == name || '\0' == *name)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
UTF8 aFolders[MOD_LBUF_SIZE];
|
||
size_t nFolders = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, player,
|
||
T("Mailfolders"), aFolders, sizeof(aFolders) - 1, &nFolders);
|
||
if (MUX_FAILED(mr) || 0 == nFolders)
|
||
{
|
||
return -1;
|
||
}
|
||
aFolders[nFolders] = '\0';
|
||
|
||
// Build upper-case pattern ":NAME:".
|
||
//
|
||
char pattern[FOLDER_NAME_LEN + 4];
|
||
pattern[0] = ':';
|
||
size_t i = 1;
|
||
const char *s = reinterpret_cast<const char *>(name);
|
||
while (*s && i < sizeof(pattern) - 2)
|
||
{
|
||
pattern[i++] = toupper(*s);
|
||
s++;
|
||
}
|
||
pattern[i++] = ':';
|
||
pattern[i] = '\0';
|
||
|
||
// Need upper-case version of aFolders for case-insensitive match.
|
||
//
|
||
char upper[MOD_LBUF_SIZE];
|
||
for (size_t k = 0; k <= nFolders; k++)
|
||
{
|
||
upper[k] = toupper(aFolders[k]);
|
||
}
|
||
|
||
const char *found = strstr(upper, pattern);
|
||
if (nullptr == found)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
// The folder number follows the closing ':'.
|
||
//
|
||
const char *p = found + strlen(pattern);
|
||
while (*p && isspace(*p))
|
||
{
|
||
p++;
|
||
}
|
||
if ('#' == *p)
|
||
{
|
||
p++;
|
||
}
|
||
return mux_atoi64(reinterpret_cast<const UTF8 *>(p));
|
||
}
|
||
|
||
int CMailMod::parse_folder(dbref player, const UTF8 *folder_string)
|
||
{
|
||
if (nullptr == folder_string || '\0' == *folder_string)
|
||
{
|
||
return -1;
|
||
}
|
||
if (isdigit(*folder_string))
|
||
{
|
||
int64_t fnum = mux_atoi64(folder_string);
|
||
if (fnum < 0 || fnum > MAX_FOLDERS)
|
||
{
|
||
return -1;
|
||
}
|
||
return fnum;
|
||
}
|
||
return get_folder_number(player, folder_string);
|
||
}
|
||
|
||
void CMailMod::add_folder_name(dbref player, int fld, const UTF8 *name)
|
||
{
|
||
if (nullptr == m_pIAttributeAccess)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UTF8 aFolders[MOD_LBUF_SIZE];
|
||
size_t nFolders = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, player,
|
||
T("Mailfolders"), aFolders, sizeof(aFolders) - 1, &nFolders);
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
nFolders = 0;
|
||
}
|
||
aFolders[nFolders] = '\0';
|
||
|
||
// Build the new record "N:NAME:N" upper-cased.
|
||
//
|
||
char record[FOLDER_NAME_LEN + 16];
|
||
mux_sprintf(reinterpret_cast<UTF8 *>(record), sizeof(record),
|
||
T("%d:"), fld);
|
||
size_t rlen = strlen(record);
|
||
const char *s = reinterpret_cast<const char *>(name);
|
||
while (*s && rlen < sizeof(record) - 8)
|
||
{
|
||
record[rlen++] = toupper(*s);
|
||
s++;
|
||
}
|
||
mux_sprintf(reinterpret_cast<UTF8 *>(record + rlen),
|
||
sizeof(record) - rlen, T(":%d"), fld);
|
||
|
||
if (0 == nFolders)
|
||
{
|
||
// No existing folders — just set the new record.
|
||
//
|
||
set_attr_checked(player, T("Mailfolders"),
|
||
reinterpret_cast<const UTF8 *>(record), T("folder list"));
|
||
return;
|
||
}
|
||
|
||
// Build pattern "N:" to find existing entry.
|
||
//
|
||
char pattern[16];
|
||
mux_sprintf(reinterpret_cast<UTF8 *>(pattern), sizeof(pattern),
|
||
T("%d:"), fld);
|
||
|
||
char *found = strstr(reinterpret_cast<char *>(aFolders), pattern);
|
||
if (nullptr != found)
|
||
{
|
||
// Replace existing entry: skip to end of old record.
|
||
//
|
||
UTF8 aNew[MOD_LBUF_SIZE];
|
||
size_t before = found - reinterpret_cast<char *>(aFolders);
|
||
memcpy(aNew, aFolders, before);
|
||
size_t nNew = before;
|
||
|
||
// Skip old record (format "N:name:N").
|
||
//
|
||
char *end = found + strlen(pattern);
|
||
while (*end && *end != ' ')
|
||
{
|
||
end++;
|
||
}
|
||
if (*end == ' ')
|
||
{
|
||
end++;
|
||
}
|
||
|
||
// Append new record.
|
||
//
|
||
size_t rsize = strlen(record);
|
||
memcpy(aNew + nNew, record, rsize);
|
||
nNew += rsize;
|
||
|
||
// Append remainder.
|
||
//
|
||
size_t remain = nFolders - (end - reinterpret_cast<char *>(aFolders));
|
||
if (remain > 0)
|
||
{
|
||
aNew[nNew++] = ' ';
|
||
memcpy(aNew + nNew, end, remain);
|
||
nNew += remain;
|
||
}
|
||
aNew[nNew] = '\0';
|
||
|
||
set_attr_checked(player, T("Mailfolders"), aNew, T("folder list"));
|
||
}
|
||
else
|
||
{
|
||
// Append new record.
|
||
//
|
||
size_t nOld = strlen(reinterpret_cast<const char *>(aFolders));
|
||
size_t nRec = strlen(record);
|
||
if (nOld + 1 + nRec < MOD_LBUF_SIZE)
|
||
{
|
||
UTF8 aNew[MOD_LBUF_SIZE];
|
||
memcpy(aNew, aFolders, nOld);
|
||
aNew[nOld] = ' ';
|
||
memcpy(aNew + nOld + 1, record, nRec + 1);
|
||
set_attr_checked(player, T("Mailfolders"), aNew,
|
||
T("folder list"));
|
||
}
|
||
}
|
||
}
|
||
|
||
bool CMailMod::mail_to_player(dbref player, struct mail *mp)
|
||
{
|
||
if (mp->to != player)
|
||
{
|
||
return false;
|
||
}
|
||
// Check that the mail postdates the player's creation (recycled dbref guard).
|
||
//
|
||
if (nullptr == m_pIAttributeAccess)
|
||
{
|
||
return true;
|
||
}
|
||
UTF8 buf[128];
|
||
size_t nLen = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, player,
|
||
T("Created"), buf, sizeof(buf) - 1, &nLen);
|
||
if (MUX_FAILED(mr) || 0 == nLen)
|
||
{
|
||
return false;
|
||
}
|
||
buf[nLen] = '\0';
|
||
CLinearTimeAbsolute ltaCreated, ltaMail;
|
||
if ( ltaCreated.SetString(buf)
|
||
&& ltaMail.SetString(reinterpret_cast<const UTF8 *>(mp->time.c_str())))
|
||
{
|
||
return ltaCreated <= ltaMail;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool CMailMod::mail_from_player(dbref player, struct mail *mp)
|
||
{
|
||
if (mp->from != player)
|
||
{
|
||
return false;
|
||
}
|
||
if (nullptr == m_pIAttributeAccess)
|
||
{
|
||
return true;
|
||
}
|
||
UTF8 buf[128];
|
||
size_t nLen = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, player,
|
||
T("Created"), buf, sizeof(buf) - 1, &nLen);
|
||
if (MUX_FAILED(mr) || 0 == nLen)
|
||
{
|
||
return false;
|
||
}
|
||
buf[nLen] = '\0';
|
||
CLinearTimeAbsolute ltaCreated, ltaMail;
|
||
if ( ltaCreated.SetString(buf)
|
||
&& ltaMail.SetString(reinterpret_cast<const UTF8 *>(mp->time.c_str())))
|
||
{
|
||
return ltaCreated <= ltaMail;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void CMailMod::count_mail_internal(dbref player, int folder,
|
||
int *rcount, int *ucount, int *ccount)
|
||
{
|
||
int rc = 0, uc = 0, cc = 0;
|
||
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
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;
|
||
}
|
||
|
||
void CMailMod::urgent_mail_internal(dbref player, int folder, int *ucount)
|
||
{
|
||
int uc = 0;
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if ( Folder(mp) == folder
|
||
&& mail_to_player(player, mp))
|
||
{
|
||
if (Unread(mp) && Urgent(mp))
|
||
{
|
||
uc++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
*ucount = uc;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Folder commands.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_change_folder(dbref player, const UTF8 *fld,
|
||
const UTF8 *newname)
|
||
{
|
||
if (nullptr == fld || '\0' == *fld)
|
||
{
|
||
DoListMailBrief(player);
|
||
return;
|
||
}
|
||
|
||
int pfld = parse_folder(player, fld);
|
||
if (pfld < 0)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: What folder is that?"));
|
||
return;
|
||
}
|
||
|
||
if (nullptr != newname && '\0' != *newname)
|
||
{
|
||
// Rename folder.
|
||
//
|
||
if (strlen(reinterpret_cast<const char *>(newname)) > FOLDER_NAME_LEN)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Folder name too long"));
|
||
return;
|
||
}
|
||
const UTF8 *p;
|
||
for (p = newname; isalnum(*p); p++)
|
||
;
|
||
if (*p != '\0')
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Illegal folder name"));
|
||
return;
|
||
}
|
||
|
||
add_folder_name(player, pfld, newname);
|
||
|
||
UTF8 msg[128];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Folder %d now named ‘%s’"),
|
||
pfld, reinterpret_cast<const char *>(newname));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
else
|
||
{
|
||
// Switch to folder.
|
||
//
|
||
set_player_folder(player, pfld);
|
||
|
||
UTF8 msg[128];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Current folder set to %d [%s]."),
|
||
pfld,
|
||
reinterpret_cast<const char *>(get_folder_name(player, pfld)));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
void CMailMod::DoListMailBrief(dbref player)
|
||
{
|
||
for (int folder = 0; folder < MAX_FOLDERS; folder++)
|
||
{
|
||
int rc, uc, cc, gc;
|
||
count_mail_internal(player, folder, &rc, &uc, &cc);
|
||
urgent_mail_internal(player, folder, &gc);
|
||
|
||
if (rc + uc > 0)
|
||
{
|
||
UTF8 msg[256];
|
||
// Three lines rather than one (#1717): four counts in one sentence
|
||
// cannot be pluralised, since MN_ chooses one form from one n.
|
||
//
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("MAIL: %d message in folder %d [%s].",
|
||
"MAIL: %d messages in folder %d [%s].", rc + uc),
|
||
rc + uc, folder,
|
||
reinterpret_cast<const char *>(get_folder_name(player, folder)));
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("MAIL: %d unread message.",
|
||
"MAIL: %d unread messages.", uc), uc);
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("MAIL: %d cleared message.",
|
||
"MAIL: %d cleared messages.", cc), cc);
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
else
|
||
{
|
||
const UTF8 *fname = get_folder_name(player, folder);
|
||
if (0 != strcmp(reinterpret_cast<const char *>(fname), "unnamed"))
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: 0 messages in folder %d [%s]."),
|
||
folder, reinterpret_cast<const char *>(fname));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
if (gc > 0)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), 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,
|
||
reinterpret_cast<const char *>(get_folder_name(player, folder)));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
int current_folder = player_folder(player);
|
||
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Current folder is %d [%s]."),
|
||
current_folder,
|
||
reinterpret_cast<const char *>(get_folder_name(player, current_folder)));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
void CMailMod::ListMailInFolderNumber(dbref player, int folder_num,
|
||
const UTF8 *msglist)
|
||
{
|
||
int original_folder = player_folder(player);
|
||
set_player_folder(player, folder_num);
|
||
|
||
struct mail_selector ms;
|
||
if (!parse_msglist(msglist, &ms, player))
|
||
{
|
||
set_player_folder(player, original_folder);
|
||
return;
|
||
}
|
||
|
||
UTF8 hdr[MOD_LBUF_SIZE];
|
||
mux_sprintf(hdr, sizeof(hdr), T("––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"–––"
|
||
" MAIL: Folder %d "
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––"),
|
||
folder_num);
|
||
m_pINotify->RawNotify(player, hdr);
|
||
|
||
int i = 0;
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (Folder(mp) == folder_num)
|
||
{
|
||
i++;
|
||
if (mail_match(mp, ms, i))
|
||
{
|
||
size_t nSize = MessageFetchSize(mp->number);
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
UTF8 line[MOD_LBUF_SIZE];
|
||
format_mail_list_line(line, sizeof(line),
|
||
status_chars(mp), i, nSize, szFromName,
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()));
|
||
m_pINotify->RawNotify(player, line);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
|
||
set_player_folder(player, original_folder);
|
||
}
|
||
|
||
void CMailMod::ListMailInFolder(dbref player, const UTF8 *folder_name,
|
||
const UTF8 *msglist)
|
||
{
|
||
int folder = 0;
|
||
|
||
if (nullptr == folder_name || '\0' == *folder_name)
|
||
{
|
||
folder = player_folder(player);
|
||
}
|
||
else
|
||
{
|
||
folder = parse_folder(player, folder_name);
|
||
}
|
||
|
||
if (-1 == folder)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No such folder."));
|
||
return;
|
||
}
|
||
ListMailInFolderNumber(player, folder, msglist);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/review — review sent mail.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_review_all(dbref player, const UTF8 *msglist)
|
||
{
|
||
int i = 0, j = 0;
|
||
|
||
if (nullptr == msglist || '\0' == *msglist)
|
||
{
|
||
// Summary mode: list all sent messages grouped by recipient.
|
||
//
|
||
for (auto &kv : m_mail_htab)
|
||
{
|
||
dbref target = kv.first;
|
||
bool bHeader = false;
|
||
|
||
for (auto &m : kv.second)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (mail_from_player(player, mp))
|
||
{
|
||
i++;
|
||
|
||
if (!bHeader)
|
||
{
|
||
UTF8 szName[64];
|
||
get_player_name(target, szName, sizeof(szName));
|
||
|
||
UTF8 hdr[MOD_LBUF_SIZE];
|
||
format_mail_to_banner(hdr, sizeof(hdr), szName);
|
||
m_pINotify->RawNotify(player, hdr);
|
||
bHeader = true;
|
||
}
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
size_t nSize = MessageFetchSize(mp->number);
|
||
UTF8 line[MOD_LBUF_SIZE];
|
||
format_mail_list_line(line, sizeof(line),
|
||
status_chars(mp), i, nSize, szFromName,
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()));
|
||
m_pINotify->RawNotify(player, line);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (0 == i)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You have no matching messages."));
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Detail mode: show full messages matching msglist.
|
||
//
|
||
struct mail_selector ms;
|
||
if (!parse_msglist(msglist, &ms, player))
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (auto &kv : m_mail_htab)
|
||
{
|
||
for (auto &m : kv.second)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (mail_from_player(player, mp))
|
||
{
|
||
i++;
|
||
if (mail_match(mp, ms, i))
|
||
{
|
||
j++;
|
||
const UTF8 *body = MessageFetch(mp->number);
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
|
||
UTF8 hdr[MOD_LBUF_SIZE];
|
||
format_mail_detail_header(hdr, sizeof(hdr),
|
||
i, szFromName,
|
||
reinterpret_cast<const UTF8 *>(mp->time.c_str()),
|
||
is_connected_visible(mp->from, player),
|
||
0,
|
||
Read(mp) ? T("Read") : T("Unread"),
|
||
Cleared(mp) ? " Cleared" : "",
|
||
Urgent(mp) ? " Urgent" : "",
|
||
Mass(mp) ? " Mass" : "",
|
||
Forward(mp) ? " Forward" : "",
|
||
M_Safe(mp) ? " Safe" : "",
|
||
Tagged(mp) ? " Tagged" : "",
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()),
|
||
65);
|
||
m_pINotify->RawNotify(player, hdr);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
m_pINotify->RawNotify(player, body);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!j)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You don’t have that many matching messages!"));
|
||
}
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_mail_review(dbref player, const UTF8 *name,
|
||
const UTF8 *msglist)
|
||
{
|
||
if ( nullptr != name
|
||
&& '\0' != *name
|
||
&& 0 == strcasecmp(reinterpret_cast<const char *>(name), "all"))
|
||
{
|
||
do_mail_review_all(player, msglist);
|
||
return;
|
||
}
|
||
|
||
// Look up the target player.
|
||
//
|
||
if (nullptr == name || '\0' == *name)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No such player."));
|
||
return;
|
||
}
|
||
|
||
dbref target = NOTHING;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
// Prefix with '*' for player lookup.
|
||
//
|
||
UTF8 namebuf[256];
|
||
mux_sprintf(namebuf, sizeof(namebuf), T("*%s"), reinterpret_cast<const char *>(name));
|
||
m_pIObjectInfo->MatchThing(player, namebuf, &target);
|
||
}
|
||
|
||
if (NOTHING == target)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No such player."));
|
||
return;
|
||
}
|
||
|
||
int i = 0, j = 0;
|
||
|
||
if (nullptr == msglist || '\0' == *msglist)
|
||
{
|
||
// Summary: list mail sent to target.
|
||
//
|
||
UTF8 szName[64];
|
||
get_player_name(target, szName, sizeof(szName));
|
||
|
||
UTF8 hdr[MOD_LBUF_SIZE];
|
||
format_mail_to_banner(hdr, sizeof(hdr), szName);
|
||
m_pINotify->RawNotify(player, hdr);
|
||
|
||
std::list<mail> *pList = MailList(target);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (mail_from_player(player, mp))
|
||
{
|
||
i++;
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
size_t nSize = MessageFetchSize(mp->number);
|
||
UTF8 line[MOD_LBUF_SIZE];
|
||
format_mail_list_line(line, sizeof(line),
|
||
status_chars(mp), i, nSize, szFromName,
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()));
|
||
m_pINotify->RawNotify(player, line);
|
||
}
|
||
}
|
||
}
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
}
|
||
else
|
||
{
|
||
// Detail mode: show full messages.
|
||
//
|
||
struct mail_selector ms;
|
||
if (!parse_msglist(msglist, &ms, target))
|
||
{
|
||
return;
|
||
}
|
||
|
||
std::list<mail> *pList = MailList(target);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (mail_from_player(player, mp))
|
||
{
|
||
i++;
|
||
if (mail_match(mp, ms, i))
|
||
{
|
||
j++;
|
||
const UTF8 *body = MessageFetch(mp->number);
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
|
||
UTF8 hdr2[MOD_LBUF_SIZE];
|
||
format_mail_detail_header(hdr2, sizeof(hdr2),
|
||
i, szFromName,
|
||
reinterpret_cast<const UTF8 *>(mp->time.c_str()),
|
||
is_connected_visible(mp->from, player),
|
||
0,
|
||
Read(mp) ? T("Read") : T("Unread"),
|
||
Cleared(mp) ? " Cleared" : "",
|
||
Urgent(mp) ? " Urgent" : "",
|
||
Mass(mp) ? " Mass" : "",
|
||
Forward(mp) ? " Forward" : "",
|
||
M_Safe(mp) ? " Safe" : "",
|
||
Tagged(mp) ? " Tagged" : "",
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()),
|
||
65);
|
||
m_pINotify->RawNotify(player, hdr2);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
m_pINotify->RawNotify(player, body);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!j)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You don’t have that many matching messages!"));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/retract — retract unread sent mail.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_retract(dbref player, const UTF8 *name,
|
||
const UTF8 *msglist)
|
||
{
|
||
if (nullptr == name || '\0' == *name)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No such player."));
|
||
return;
|
||
}
|
||
|
||
// Handle malias target.
|
||
//
|
||
if (*name == '*')
|
||
{
|
||
int nResult;
|
||
malias_t *m = get_malias(player, name, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Mail alias %s not found."),
|
||
reinterpret_cast<const char *>(name));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if (nResult == GMA_FOUND)
|
||
{
|
||
for (size_t k = 0; k < m->list.size(); k++)
|
||
{
|
||
UTF8 dbrefbuf[32];
|
||
mux_sprintf(dbrefbuf, sizeof(dbrefbuf), T("#%d"), m->list[k]);
|
||
do_mail_retract(player, dbrefbuf, msglist);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Look up target player.
|
||
//
|
||
dbref target = NOTHING;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
UTF8 namebuf[256];
|
||
mux_sprintf(namebuf, sizeof(namebuf), T("*%s"), reinterpret_cast<const char *>(name));
|
||
m_pIObjectInfo->MatchThing(player, namebuf, &target);
|
||
}
|
||
|
||
if (NOTHING == target)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No such player."));
|
||
return;
|
||
}
|
||
|
||
struct mail_selector ms;
|
||
if (!parse_msglist(msglist, &ms, target))
|
||
{
|
||
return;
|
||
}
|
||
|
||
int i = 0, j = 0;
|
||
std::list<mail> *pList = MailList(target);
|
||
if (pList)
|
||
{
|
||
for (auto it = pList->begin(); it != pList->end(); )
|
||
{
|
||
struct mail *mp = &*it;
|
||
if (mail_from_player(player, mp))
|
||
{
|
||
i++;
|
||
if (mail_match(mp, ms, i))
|
||
{
|
||
j++;
|
||
if (Unread(mp))
|
||
{
|
||
sqlite_wt_delete_mail(mp);
|
||
MessageReferenceDec(mp->number);
|
||
it = pList->erase(it);
|
||
m_pINotify->RawNotify(player, M_("MAIL: Mail retracted."));
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: That message has been read."));
|
||
}
|
||
}
|
||
}
|
||
++it;
|
||
}
|
||
if (pList->empty()) m_mail_htab.erase(target);
|
||
}
|
||
|
||
if (!j)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No matching messages."));
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/nuke — admin: clear all mail in the database.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_nuke(dbref player)
|
||
{
|
||
bool bGod = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsGod(player, &bGod);
|
||
}
|
||
if (!bGod)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("The postal service issues a warrant for your arrest."));
|
||
return;
|
||
}
|
||
|
||
// Remove all mail from all players.
|
||
//
|
||
for (auto it = m_mail_htab.begin(); it != m_mail_htab.end(); )
|
||
{
|
||
dbref target = it->first;
|
||
// Decrement refs before erasing.
|
||
//
|
||
for (auto &m : it->second)
|
||
{
|
||
MessageReferenceDec(m.number);
|
||
}
|
||
sqlite_wt_delete_all_mail(target);
|
||
it = m_mail_htab.erase(it);
|
||
}
|
||
|
||
UTF8 msg[128];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: ** MAIL PURGE ** done by #%d."), player);
|
||
if (nullptr != m_pILog)
|
||
{
|
||
m_pILog->log_text(msg);
|
||
}
|
||
m_pINotify->RawNotify(player,
|
||
M_("You annihilate the post office. All messages cleared."));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Delivery pipeline — send_mail, mail_to_list, make_numlist, make_namelist.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
struct mail *CMailMod::mail_fetch(dbref player, int num)
|
||
{
|
||
int i = 0;
|
||
int fld = player_folder(player);
|
||
std::list<mail> *pList = MailList(player);
|
||
if (!pList)
|
||
{
|
||
return nullptr;
|
||
}
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if ( Folder(mp) == fld
|
||
&& mail_to_player(player, mp))
|
||
{
|
||
i++;
|
||
if (i == num)
|
||
{
|
||
return mp;
|
||
}
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
void CMailMod::send_mail
|
||
(
|
||
dbref player,
|
||
dbref target,
|
||
const UTF8 *tolist,
|
||
const UTF8 *subject,
|
||
int number,
|
||
mail_flag flags,
|
||
bool silent
|
||
)
|
||
{
|
||
// Verify target is a player.
|
||
//
|
||
bool bIsPlayer = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(target, &bIsPlayer);
|
||
}
|
||
if (!bIsPlayer)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You cannot send mail to non-existent people."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Check mail lock via IMailDelivery.
|
||
//
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
bool bOk = false;
|
||
m_pIMailDelivery->MailCheck(player, target, &bOk);
|
||
if (!bOk)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
CLinearTimeAbsolute ltaNow;
|
||
ltaNow.GetLocal();
|
||
const UTF8 *pTimeStr = ltaNow.ReturnDateString(0);
|
||
|
||
// Determine sender: if player is not a player, use owner if owner
|
||
// is a wizard.
|
||
//
|
||
dbref from_player;
|
||
{
|
||
// Check if player itself is a player.
|
||
//
|
||
bool bSenderIsPlayer = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(player, &bSenderIsPlayer);
|
||
}
|
||
if (bSenderIsPlayer)
|
||
{
|
||
from_player = player;
|
||
}
|
||
else
|
||
{
|
||
dbref mailbag = -1;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetOwner(player, &mailbag);
|
||
}
|
||
bool bWiz = false;
|
||
if (nullptr != m_pIPermissions && mailbag >= 0)
|
||
{
|
||
m_pIPermissions->IsWizard(mailbag, &bWiz);
|
||
}
|
||
from_player = bWiz ? player : mailbag;
|
||
}
|
||
}
|
||
|
||
// Build the mail struct and append first (#1196: engine counts after
|
||
// emplace so the limit is total > max, not total >= max before insert).
|
||
//
|
||
mail newm;
|
||
newm.to = target;
|
||
newm.from = from_player;
|
||
newm.sqlite_id = -1;
|
||
|
||
if (!tolist || tolist[0] == '\0')
|
||
{
|
||
newm.tolist = "*HIDDEN*";
|
||
}
|
||
else
|
||
{
|
||
newm.tolist = reinterpret_cast<const char *>(tolist);
|
||
}
|
||
|
||
newm.number = number;
|
||
MessageReferenceInc(number);
|
||
newm.time = reinterpret_cast<const char *>(pTimeStr);
|
||
newm.subject = reinterpret_cast<const char *>(subject);
|
||
|
||
// Send to folder 0.
|
||
//
|
||
newm.read = flags & M_FMASK;
|
||
|
||
m_mail_htab[target].push_back(std::move(newm));
|
||
struct mail *mp = &m_mail_htab[target].back();
|
||
|
||
// Reject if the target's mailbox is full. Engine uses No_Mail_Expire
|
||
// (wizard or POW_NO_MAIL_EXPIRE); COM only exposes IsWizard today.
|
||
//
|
||
if (0 < m_mail_per_player)
|
||
{
|
||
bool bExempt = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsWizard(target, &bExempt);
|
||
}
|
||
if (!bExempt)
|
||
{
|
||
int total = static_cast<int>(m_mail_htab[target].size());
|
||
if (total > m_mail_per_player)
|
||
{
|
||
UTF8 targetname[MOD_LBUF_SIZE];
|
||
get_player_name(target, targetname, sizeof(targetname));
|
||
|
||
UTF8 msg[2 * MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg), MN_(
|
||
"MAIL: %s’s mailbox is full (%d message).",
|
||
"MAIL: %s’s mailbox is full (%d messages).",
|
||
total - 1),
|
||
targetname, total - 1);
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
MessageReferenceDec(number);
|
||
m_mail_htab[target].pop_back();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Persist and notify.
|
||
//
|
||
sqlite_wt_insert_mail(mp);
|
||
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->NotifyDelivery(mp->from, target, subject, silent);
|
||
}
|
||
}
|
||
|
||
// make_numlist — resolve space-separated player names/aliases to dbref numbers.
|
||
//
|
||
// Returns a std::string of space-separated dbref numbers, or empty on error.
|
||
//
|
||
std::string CMailMod::make_numlist(dbref player, const UTF8 *arg, bool bBlind)
|
||
{
|
||
if (!arg || !*arg)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No players specified."));
|
||
}
|
||
return std::string();
|
||
}
|
||
|
||
dbref aRecip[4000];
|
||
int nRecip = 0;
|
||
|
||
// Work on a mutable copy.
|
||
//
|
||
UTF8 buf[MOD_LBUF_SIZE];
|
||
strncpy(reinterpret_cast<char *>(buf),
|
||
reinterpret_cast<const char *>(arg), MOD_LBUF_SIZE - 1);
|
||
buf[MOD_LBUF_SIZE - 1] = '\0';
|
||
|
||
UTF8 *head = buf;
|
||
while (head && *head)
|
||
{
|
||
while (*head == ' ')
|
||
{
|
||
head++;
|
||
}
|
||
if (!*head)
|
||
{
|
||
break;
|
||
}
|
||
|
||
UTF8 *tail = head;
|
||
while (*tail && *tail != ' ')
|
||
{
|
||
if (*tail == '"')
|
||
{
|
||
head++;
|
||
tail++;
|
||
while (*tail && *tail != '"')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
if (*tail)
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
// Back up to strip a trailing closing quote if present, but only
|
||
// when there is content between head and tail; a lone '"' leaves
|
||
// tail == head and must not be decremented (would underflow).
|
||
if (tail > head)
|
||
{
|
||
tail--;
|
||
if (*tail != '"')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
UTF8 spot = *tail;
|
||
*tail = '\0';
|
||
|
||
if (*head == '*')
|
||
{
|
||
// Malias expansion.
|
||
//
|
||
int nResult;
|
||
malias_t *m = get_malias(player, head, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[2 * MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ does not exist."),
|
||
reinterpret_cast<const char *>(head));
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return std::string();
|
||
}
|
||
else if (nResult == GMA_INVALIDFORM)
|
||
{
|
||
UTF8 msg[2 * MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: ‘%s’ is a badly-formed alias."),
|
||
reinterpret_cast<const char *>(head));
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return std::string();
|
||
}
|
||
for (size_t i = 0; i < m->list.size() && nRecip < 4000; i++)
|
||
{
|
||
aRecip[nRecip++] = m->list[i];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Player lookup via IObjectInfo::MatchThing.
|
||
//
|
||
UTF8 lookup[MOD_LBUF_SIZE + 1];
|
||
if (*head != '*')
|
||
{
|
||
mux_sprintf(lookup, sizeof(lookup), T("*%s"), reinterpret_cast<const char *>(head));
|
||
}
|
||
else
|
||
{
|
||
strncpy(reinterpret_cast<char *>(lookup),
|
||
reinterpret_cast<const char *>(head), sizeof(lookup) - 1);
|
||
lookup[sizeof(lookup) - 1] = '\0';
|
||
}
|
||
|
||
dbref target = NOTHING;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->MatchThing(player, lookup, &target);
|
||
}
|
||
if (target >= 0)
|
||
{
|
||
if (nRecip < 4000)
|
||
{
|
||
aRecip[nRecip++] = target;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
UTF8 msg[2 * MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: ‘%s’ does not exist."),
|
||
reinterpret_cast<const char *>(head));
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return std::string();
|
||
}
|
||
}
|
||
|
||
*tail = spot;
|
||
head = tail;
|
||
if (*head == '"')
|
||
{
|
||
head++;
|
||
}
|
||
}
|
||
|
||
if (nRecip <= 0)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No players specified."));
|
||
}
|
||
return std::string();
|
||
}
|
||
|
||
// De-duplicate and build result string.
|
||
// #1066: never advance by snprintf's untruncated return value — that
|
||
// walks past the stack buffer when the list is truncated.
|
||
//
|
||
UTF8 result[MOD_LBUF_SIZE];
|
||
UTF8 *rp = result;
|
||
for (int i = 0; i < nRecip; i++)
|
||
{
|
||
if (aRecip[i] != NOTHING)
|
||
{
|
||
// Remove duplicates.
|
||
//
|
||
for (int j = i + 1; j < nRecip; j++)
|
||
{
|
||
if (aRecip[i] == aRecip[j])
|
||
{
|
||
aRecip[j] = NOTHING;
|
||
}
|
||
}
|
||
|
||
size_t remain = sizeof(result) - static_cast<size_t>(rp - result);
|
||
if (remain <= 1)
|
||
{
|
||
break;
|
||
}
|
||
if (rp != result)
|
||
{
|
||
*rp++ = ' ';
|
||
remain--;
|
||
}
|
||
if (bBlind)
|
||
{
|
||
if (remain <= 1)
|
||
{
|
||
break;
|
||
}
|
||
*rp++ = '!';
|
||
remain--;
|
||
}
|
||
size_t n = mail_sprintf(rp, remain, T("%d"), aRecip[i]);
|
||
if (n + 1 >= remain)
|
||
{
|
||
// Truncated incomplete number — discard it.
|
||
*rp = '\0';
|
||
break;
|
||
}
|
||
rp += n;
|
||
}
|
||
}
|
||
*rp = '\0';
|
||
|
||
return std::string(reinterpret_cast<const char *>(result));
|
||
}
|
||
|
||
// make_namelist — convert space-separated dbref numbers to player names.
|
||
//
|
||
std::string CMailMod::make_namelist(dbref player, const UTF8 *arg)
|
||
{
|
||
UNUSED_PARAMETER(player);
|
||
|
||
if (!arg || !*arg)
|
||
{
|
||
return std::string();
|
||
}
|
||
|
||
UTF8 buf[MOD_LBUF_SIZE];
|
||
strncpy(reinterpret_cast<char *>(buf),
|
||
reinterpret_cast<const char *>(arg), MOD_LBUF_SIZE - 1);
|
||
buf[MOD_LBUF_SIZE - 1] = '\0';
|
||
|
||
UTF8 result[MOD_LBUF_SIZE];
|
||
UTF8 *rp = result;
|
||
bool bFirst = true;
|
||
|
||
char *saveptr = nullptr;
|
||
char *token = strtok_r(reinterpret_cast<char *>(buf), " ", &saveptr);
|
||
while (token)
|
||
{
|
||
if (!bFirst)
|
||
{
|
||
size_t remain = sizeof(result) - (rp - result);
|
||
if (remain > 2)
|
||
{
|
||
*rp++ = ',';
|
||
*rp++ = ' ';
|
||
}
|
||
}
|
||
bFirst = false;
|
||
|
||
char *p = token;
|
||
bool bBCC = false;
|
||
if (*p == '!')
|
||
{
|
||
bBCC = true;
|
||
p++;
|
||
}
|
||
|
||
if (isdigit(static_cast<unsigned char>(*p)))
|
||
{
|
||
dbref target = static_cast<dbref>(mux_atoi64(reinterpret_cast<const UTF8 *>(p)));
|
||
UTF8 name[MOD_LBUF_SIZE];
|
||
get_player_name(target, name, sizeof(name));
|
||
|
||
if (bBCC)
|
||
{
|
||
size_t remain = sizeof(result) - (rp - result);
|
||
if (remain > 1)
|
||
{
|
||
*rp++ = '!';
|
||
}
|
||
}
|
||
size_t nlen = strlen(reinterpret_cast<const char *>(name));
|
||
size_t remain = sizeof(result) - (rp - result);
|
||
if (nlen < remain)
|
||
{
|
||
memcpy(rp, name, nlen);
|
||
rp += nlen;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
size_t tlen = strlen(token);
|
||
size_t remain = sizeof(result) - (rp - result);
|
||
if (tlen < remain)
|
||
{
|
||
memcpy(rp, token, tlen);
|
||
rp += tlen;
|
||
}
|
||
}
|
||
|
||
token = strtok_r(nullptr, " ", &saveptr);
|
||
}
|
||
*rp = '\0';
|
||
|
||
return std::string(reinterpret_cast<const char *>(result));
|
||
}
|
||
|
||
// mail_to_list — send mail to all recipients in a numlist.
|
||
//
|
||
void CMailMod::mail_to_list(dbref player, UTF8 *list, const UTF8 *subject,
|
||
const UTF8 *message, mail_flag flags, bool silent)
|
||
{
|
||
if (!list)
|
||
{
|
||
return;
|
||
}
|
||
if (!*list)
|
||
{
|
||
free(list);
|
||
return;
|
||
}
|
||
|
||
// Build tolist (excluding BCC) and senderlist (including BCC).
|
||
//
|
||
UTF8 tolist[MOD_LBUF_SIZE];
|
||
UTF8 *p = tolist;
|
||
UTF8 senderlist[MOD_LBUF_SIZE];
|
||
UTF8 *sp = senderlist;
|
||
UTF8 *head = list;
|
||
|
||
while (*head)
|
||
{
|
||
while (*head == ' ')
|
||
{
|
||
head++;
|
||
}
|
||
if (!*head)
|
||
{
|
||
break;
|
||
}
|
||
|
||
UTF8 *tail = head;
|
||
while (*tail && *tail != ' ')
|
||
{
|
||
if (*tail == '"')
|
||
{
|
||
head++;
|
||
tail++;
|
||
while (*tail && *tail != '"')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
if (*tail)
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
// Back up to strip a trailing closing quote if present, but only
|
||
// when there is content between head and tail; a lone '"' leaves
|
||
// tail == head and must not be decremented (would underflow).
|
||
if (tail > head)
|
||
{
|
||
tail--;
|
||
if (*tail != '"')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
|
||
// Append to senderlist.
|
||
//
|
||
size_t seglen = tail - head;
|
||
if (sp != senderlist)
|
||
{
|
||
size_t remain = sizeof(senderlist) - (sp - senderlist);
|
||
if (remain > 1)
|
||
{
|
||
*sp++ = ' ';
|
||
}
|
||
}
|
||
{
|
||
size_t remain = sizeof(senderlist) - (sp - senderlist);
|
||
if (seglen < remain)
|
||
{
|
||
memcpy(sp, head, seglen);
|
||
sp += seglen;
|
||
}
|
||
}
|
||
|
||
// Append to tolist if not BCC (!-prefixed).
|
||
//
|
||
if (*head != '!')
|
||
{
|
||
if (p != tolist)
|
||
{
|
||
size_t remain = sizeof(tolist) - (p - tolist);
|
||
if (remain > 1)
|
||
{
|
||
*p++ = ' ';
|
||
}
|
||
}
|
||
size_t remain = sizeof(tolist) - (p - tolist);
|
||
if (seglen < remain)
|
||
{
|
||
memcpy(p, head, seglen);
|
||
p += seglen;
|
||
}
|
||
}
|
||
|
||
head = tail;
|
||
if (*head == '"')
|
||
{
|
||
head++;
|
||
}
|
||
}
|
||
*p = '\0';
|
||
*sp = '\0';
|
||
|
||
// Add the message body (with signature evaluation).
|
||
//
|
||
int number = add_mail_message(player, message);
|
||
if (number == NOTHING)
|
||
{
|
||
free(list);
|
||
return;
|
||
}
|
||
|
||
// Iterate recipients and send.
|
||
//
|
||
head = list;
|
||
while (*head)
|
||
{
|
||
while (*head == ' ')
|
||
{
|
||
head++;
|
||
}
|
||
if (!*head)
|
||
{
|
||
break;
|
||
}
|
||
|
||
UTF8 *tail = head;
|
||
while (*tail && *tail != ' ')
|
||
{
|
||
if (*tail == '"')
|
||
{
|
||
head++;
|
||
tail++;
|
||
while (*tail && *tail != '"')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
if (*tail)
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
// Back up to strip a trailing closing quote if present, but only
|
||
// when there is content between head and tail; a lone '"' leaves
|
||
// tail == head and must not be decremented (would underflow).
|
||
if (tail > head)
|
||
{
|
||
tail--;
|
||
if (*tail != '"')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
UTF8 spot = *tail;
|
||
*tail = '\0';
|
||
|
||
if (*head == '!')
|
||
{
|
||
head++;
|
||
}
|
||
|
||
if (*head == '*')
|
||
{
|
||
// Malias — expand and send to each member.
|
||
//
|
||
int nResult;
|
||
malias_t *m = get_malias(player, head, &nResult);
|
||
if (nResult == GMA_FOUND && nullptr != m)
|
||
{
|
||
for (size_t i = 0; i < m->list.size(); i++)
|
||
{
|
||
bool bTargetIsPlayer = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(m->list[i], &bTargetIsPlayer);
|
||
}
|
||
if (bTargetIsPlayer)
|
||
{
|
||
send_mail(player, m->list[i],
|
||
(m->list[i] == player) ? senderlist : tolist,
|
||
subject, number, flags, silent);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
dbref target = static_cast<dbref>(mux_atoi64(head));
|
||
bool bTargetIsPlayer = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(target, &bTargetIsPlayer);
|
||
}
|
||
if (bTargetIsPlayer)
|
||
{
|
||
send_mail(player, target,
|
||
(target == player) ? senderlist : tolist,
|
||
subject, number, flags, silent);
|
||
}
|
||
}
|
||
|
||
*tail = spot;
|
||
head = tail;
|
||
if (*head == '"')
|
||
{
|
||
head++;
|
||
}
|
||
}
|
||
MessageReferenceDec(number);
|
||
free(list);
|
||
}
|
||
|
||
// add_mail_message — store a message body, optionally appending signature.
|
||
//
|
||
// This is the module equivalent of the server's add_mail_message(). It reads
|
||
// A_SIGNATURE via IAttributeAccess and evaluates it via IEvaluator.
|
||
//
|
||
int CMailMod::add_mail_message(dbref player, const UTF8 *message)
|
||
{
|
||
// Reject if message says "clear" (common user error).
|
||
//
|
||
if (0 == strcasecmp(reinterpret_cast<const char *>(message), "clear"))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You probably did not intend to send a @mail saying ‘clear’."));
|
||
}
|
||
return NOTHING;
|
||
}
|
||
|
||
// Read and evaluate signature.
|
||
//
|
||
UTF8 sigraw[MOD_LBUF_SIZE];
|
||
sigraw[0] = '\0';
|
||
if (nullptr != m_pIAttributeAccess)
|
||
{
|
||
size_t nLen = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, player,
|
||
T("Signature"), sigraw, sizeof(sigraw) - 1, &nLen);
|
||
if (MUX_SUCCEEDED(mr))
|
||
{
|
||
sigraw[nLen] = '\0';
|
||
}
|
||
else
|
||
{
|
||
sigraw[0] = '\0';
|
||
}
|
||
}
|
||
|
||
// Evaluate the signature if non-empty.
|
||
//
|
||
UTF8 sigeval[MOD_LBUF_SIZE];
|
||
sigeval[0] = '\0';
|
||
if (sigraw[0] != '\0' && nullptr != m_pIMailDelivery)
|
||
{
|
||
// Use IEvaluator if available; otherwise use raw signature.
|
||
//
|
||
mux_IEvaluator *pIEval = nullptr;
|
||
MUX_RESULT mr = mux_CreateInstance(CID_Evaluator, nullptr,
|
||
UseSameProcess, IID_IEvaluator,
|
||
reinterpret_cast<void **>(&pIEval));
|
||
if (MUX_SUCCEEDED(mr) && nullptr != pIEval)
|
||
{
|
||
size_t nResultLen = 0;
|
||
pIEval->Eval(player, player, player, sigraw,
|
||
sigeval, sizeof(sigeval) - 1, &nResultLen);
|
||
sigeval[nResultLen] = '\0';
|
||
pIEval->Release();
|
||
}
|
||
else
|
||
{
|
||
strncpy(reinterpret_cast<char *>(sigeval),
|
||
reinterpret_cast<const char *>(sigraw),
|
||
sizeof(sigeval) - 1);
|
||
sigeval[sizeof(sigeval) - 1] = '\0';
|
||
}
|
||
}
|
||
|
||
// Combine message + signature.
|
||
//
|
||
UTF8 combined[MOD_LBUF_SIZE];
|
||
if (sigeval[0] != '\0')
|
||
{
|
||
mux_sprintf(combined, sizeof(combined), T("%s %s"),
|
||
reinterpret_cast<const char *>(message),
|
||
reinterpret_cast<const char *>(sigeval));
|
||
}
|
||
else
|
||
{
|
||
strncpy(reinterpret_cast<char *>(combined),
|
||
reinterpret_cast<const char *>(message),
|
||
sizeof(combined) - 1);
|
||
combined[sizeof(combined) - 1] = '\0';
|
||
}
|
||
|
||
return new_mail_message(combined, NOTHING);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Composition commands.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_expmail_start(dbref player, const UTF8 *arg,
|
||
const UTF8 *subject)
|
||
{
|
||
if (!arg || !*arg)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: I do not know whom you want to mail."));
|
||
}
|
||
return;
|
||
}
|
||
if (!subject || !*subject)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No subject."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Check if already composing.
|
||
//
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
bool bComposing = false;
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
if (bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Mail message already in progress."));
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Throttle check.
|
||
//
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
bool bWiz = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsWizard(player, &bWiz);
|
||
}
|
||
if (!bWiz)
|
||
{
|
||
bool bThrottled = false;
|
||
m_pIMailDelivery->ThrottleCheck(player, &bThrottled);
|
||
if (bThrottled)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Too much @mail sent recently."));
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Resolve recipients.
|
||
//
|
||
std::string tolist = make_numlist(player, arg, false);
|
||
if (tolist.empty())
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Store composition state. #1229: Mailto/Mailsub/Mailmsg/Mailflags are
|
||
// AF_INTERNAL or AF_DARK — IAttributeAccess permission checks always
|
||
// reject player-as-executor writes; use raw attr APIs like the engine.
|
||
//
|
||
set_comp_attr(player, MOD_A_MAILTO,
|
||
reinterpret_cast<const UTF8 *>(tolist.c_str()));
|
||
set_comp_attr(player, MOD_A_MAILSUB, subject);
|
||
set_comp_attr(player, MOD_A_MAILFLAGS, T("0"));
|
||
clr_comp_attr(player, MOD_A_MAILMSG);
|
||
|
||
// Set composing flag.
|
||
//
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->SetComposing(player, true);
|
||
}
|
||
|
||
// Notify with recipient names.
|
||
//
|
||
std::string names = make_namelist(player,
|
||
reinterpret_cast<const UTF8 *>(tolist.c_str()));
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: You are sending mail to ‘%s’."),
|
||
names.c_str());
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_expmail_stop(dbref player, int flags)
|
||
{
|
||
// Check composing state.
|
||
//
|
||
bool bComposing = false;
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
}
|
||
if (!bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No message started."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Read composition attributes.
|
||
//
|
||
UTF8 aTolist[MOD_LBUF_SIZE];
|
||
aTolist[0] = '\0';
|
||
UTF8 aMailMsg[MOD_LBUF_SIZE];
|
||
aMailMsg[0] = '\0';
|
||
UTF8 aMailSub[MOD_LBUF_SIZE];
|
||
aMailSub[0] = '\0';
|
||
UTF8 aMailFlags[MOD_LBUF_SIZE];
|
||
aMailFlags[0] = '\0';
|
||
|
||
get_comp_attr(player, MOD_A_MAILTO, aTolist, sizeof(aTolist));
|
||
get_comp_attr(player, MOD_A_MAILMSG, aMailMsg, sizeof(aMailMsg));
|
||
get_comp_attr(player, MOD_A_MAILSUB, aMailSub, sizeof(aMailSub));
|
||
get_comp_attr(player, MOD_A_MAILFLAGS, aMailFlags, sizeof(aMailFlags));
|
||
|
||
if (aTolist[0] == '\0')
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No recipients."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (aMailMsg[0] == '\0')
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: The body of this message is empty. Use - to add to the message."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Build numlist copy for mail_to_list (which takes ownership).
|
||
//
|
||
UTF8 *tolist_copy = reinterpret_cast<UTF8 *>(
|
||
strdup(reinterpret_cast<const char *>(aTolist)));
|
||
|
||
int combinedFlags = flags | static_cast<int>(mux_atoi64(aMailFlags));
|
||
mail_to_list(player, tolist_copy, aMailSub, aMailMsg, combinedFlags, false);
|
||
|
||
// Clear composing flag.
|
||
//
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->SetComposing(player, false);
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_expmail_abort(dbref player)
|
||
{
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->SetComposing(player, false);
|
||
}
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Message aborted."));
|
||
}
|
||
}
|
||
|
||
// #1196: append recipients to an in-progress composition (CC/BCC).
|
||
// bBlind prefixes each resolved dbref with '!' so make_namelist/send hide them.
|
||
//
|
||
void CMailMod::do_mail_cc(dbref player, const UTF8 *arg, bool bBlind)
|
||
{
|
||
bool bComposing = false;
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
}
|
||
if (!bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: No mail message in progress."));
|
||
}
|
||
return;
|
||
}
|
||
if (nullptr == arg || '\0' == *arg)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: I do not know whom you want to mail."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
std::string tolist = make_numlist(player, arg, bBlind);
|
||
if (tolist.empty())
|
||
{
|
||
return;
|
||
}
|
||
|
||
UTF8 existing[MOD_LBUF_SIZE];
|
||
existing[0] = '\0';
|
||
get_comp_attr(player, MOD_A_MAILTO, existing, sizeof(existing));
|
||
|
||
std::string fulllist = tolist;
|
||
if ('\0' != existing[0])
|
||
{
|
||
fulllist.push_back(' ');
|
||
fulllist.append(reinterpret_cast<const char *>(existing));
|
||
}
|
||
|
||
set_comp_attr(player, MOD_A_MAILTO,
|
||
reinterpret_cast<const UTF8 *>(fulllist.c_str()));
|
||
|
||
std::string names = make_namelist(player,
|
||
reinterpret_cast<const UTF8 *>(fulllist.c_str()));
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: You are sending mail to ‘%s’."),
|
||
names.c_str());
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_mail_quick(dbref player, const UTF8 *arg1,
|
||
const UTF8 *arg2)
|
||
{
|
||
if (!arg1 || !*arg1)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: I don’t know who you want to mail."));
|
||
}
|
||
return;
|
||
}
|
||
if (!arg2 || !*arg2)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No message."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Check composing state.
|
||
//
|
||
bool bComposing = false;
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
}
|
||
if (bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Mail message already in progress."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Throttle check.
|
||
//
|
||
bool bWiz = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsWizard(player, &bWiz);
|
||
}
|
||
if (!bWiz && nullptr != m_pIMailDelivery)
|
||
{
|
||
bool bThrottled = false;
|
||
m_pIMailDelivery->ThrottleCheck(player, &bThrottled);
|
||
if (bThrottled)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Too much @mail sent recently."));
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Parse "recipients/subject" from arg1.
|
||
//
|
||
UTF8 bufDest[MOD_LBUF_SIZE];
|
||
strncpy(reinterpret_cast<char *>(bufDest),
|
||
reinterpret_cast<const char *>(arg1),
|
||
sizeof(bufDest) - 1);
|
||
bufDest[sizeof(bufDest) - 1] = '\0';
|
||
|
||
UTF8 *pSubject = reinterpret_cast<UTF8 *>(
|
||
strchr(reinterpret_cast<char *>(bufDest), '/'));
|
||
if (!pSubject)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No subject."));
|
||
}
|
||
return;
|
||
}
|
||
*pSubject++ = '\0';
|
||
|
||
std::string numlist = make_numlist(player, bufDest, false);
|
||
if (numlist.empty())
|
||
{
|
||
return;
|
||
}
|
||
// mail_to_list takes ownership of a malloc'd buffer.
|
||
//
|
||
UTF8 *numlist_copy = reinterpret_cast<UTF8 *>(strdup(numlist.c_str()));
|
||
if (nullptr == numlist_copy)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Out of memory, message not sent."));
|
||
}
|
||
return;
|
||
}
|
||
mail_to_list(player, numlist_copy, pSubject, arg2, 0, false);
|
||
}
|
||
|
||
void CMailMod::do_mail_fwd(dbref player, const UTF8 *msg,
|
||
const UTF8 *tolist)
|
||
{
|
||
// Check composing state.
|
||
//
|
||
bool bComposing = false;
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
}
|
||
if (bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Mail message already in progress."));
|
||
}
|
||
return;
|
||
}
|
||
if (!msg || !*msg)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No message list."));
|
||
}
|
||
return;
|
||
}
|
||
if (!tolist || !*tolist)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: To whom should I forward?"));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Throttle check.
|
||
//
|
||
bool bWiz = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsWizard(player, &bWiz);
|
||
}
|
||
if (!bWiz && nullptr != m_pIMailDelivery)
|
||
{
|
||
bool bThrottled = false;
|
||
m_pIMailDelivery->ThrottleCheck(player, &bThrottled);
|
||
if (bThrottled)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Too much @mail sent recently."));
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
int64_t num = mux_atoi64(msg);
|
||
if (!num)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: I don’t understand that message number."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
struct mail *mp = mail_fetch(player, num);
|
||
if (!mp)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You can’t forward non-existent messages."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Build subject: "Original Subject (fwd from SenderName)"
|
||
//
|
||
UTF8 fromname[MOD_LBUF_SIZE];
|
||
get_player_name(mp->from, fromname, sizeof(fromname));
|
||
UTF8 subj[2 * MOD_LBUF_SIZE];
|
||
mux_sprintf(subj, sizeof(subj), M_("%s (fwd from %s)"),
|
||
mp->subject.c_str(),
|
||
reinterpret_cast<const char *>(fromname));
|
||
|
||
do_expmail_start(player, tolist, subj);
|
||
|
||
// Set message body to the forwarded message.
|
||
//
|
||
const UTF8 *body = MessageFetch(mp->number);
|
||
if (body)
|
||
{
|
||
set_comp_attr(player, MOD_A_MAILMSG, body);
|
||
}
|
||
|
||
// Set M_FORWARD flag.
|
||
//
|
||
UTF8 aFlags[32];
|
||
aFlags[0] = '\0';
|
||
get_comp_attr(player, MOD_A_MAILFLAGS, aFlags, sizeof(aFlags));
|
||
int iFlag = M_FORWARD;
|
||
if (aFlags[0])
|
||
{
|
||
iFlag |= static_cast<int>(mux_atoi64(aFlags));
|
||
}
|
||
UTF8 flagbuf[16];
|
||
mux_sprintf(flagbuf, sizeof(flagbuf), T("%d"), iFlag);
|
||
set_comp_attr(player, MOD_A_MAILFLAGS, flagbuf);
|
||
}
|
||
|
||
void CMailMod::do_mail_reply(dbref player, const UTF8 *msg, bool all,
|
||
int key)
|
||
{
|
||
// Check composing state.
|
||
//
|
||
bool bComposing = false;
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
}
|
||
if (bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Mail message already in progress."));
|
||
}
|
||
return;
|
||
}
|
||
if (!msg || !*msg)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No message list."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Throttle check.
|
||
//
|
||
bool bWiz = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsWizard(player, &bWiz);
|
||
}
|
||
if (!bWiz && nullptr != m_pIMailDelivery)
|
||
{
|
||
bool bThrottled = false;
|
||
m_pIMailDelivery->ThrottleCheck(player, &bThrottled);
|
||
if (bThrottled)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Too much @mail sent recently."));
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
int64_t num = mux_atoi64(msg);
|
||
if (!num)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: I don’t understand that message number."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
struct mail *mp = mail_fetch(player, num);
|
||
if (!mp)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You can’t reply to non-existent messages."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!mail_from_player(mp->from, mp))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: The original sender no longer exists."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Build recipient list.
|
||
//
|
||
UTF8 tolist[MOD_LBUF_SIZE];
|
||
UTF8 *bp = tolist;
|
||
|
||
if (all)
|
||
{
|
||
// Include all original recipients except the original sender.
|
||
//
|
||
UTF8 oldlist[MOD_LBUF_SIZE];
|
||
strncpy(reinterpret_cast<char *>(oldlist),
|
||
mp->tolist.c_str(),
|
||
sizeof(oldlist) - 1);
|
||
oldlist[sizeof(oldlist) - 1] = '\0';
|
||
|
||
char *saveptr = nullptr;
|
||
char *token = strtok_r(reinterpret_cast<char *>(oldlist),
|
||
" ", &saveptr);
|
||
while (token)
|
||
{
|
||
if (mux_atoi64(reinterpret_cast<const UTF8 *>(token)) != mp->from)
|
||
{
|
||
if (bp != tolist)
|
||
{
|
||
*bp++ = ' ';
|
||
}
|
||
*bp++ = '#';
|
||
size_t tlen = strlen(token);
|
||
size_t remain = sizeof(tolist) - (bp - tolist);
|
||
if (tlen < remain)
|
||
{
|
||
memcpy(bp, token, tlen);
|
||
bp += tlen;
|
||
}
|
||
}
|
||
token = strtok_r(nullptr, " ", &saveptr);
|
||
}
|
||
|
||
// Add original sender.
|
||
//
|
||
if (bp != tolist)
|
||
{
|
||
*bp++ = ' ';
|
||
}
|
||
bp += mail_sprintf(bp, sizeof(tolist) - (bp - tolist),
|
||
T("#%d"), mp->from);
|
||
}
|
||
else
|
||
{
|
||
bp += mail_sprintf(bp, sizeof(tolist) - (bp - tolist),
|
||
T("#%d"), mp->from);
|
||
}
|
||
*bp = '\0';
|
||
|
||
// Build subject.
|
||
//
|
||
const char *pSubject = mp->subject.c_str();
|
||
UTF8 subj[MOD_LBUF_SIZE];
|
||
if (strncmp(pSubject, "Re:", 3))
|
||
{
|
||
mux_sprintf(subj, sizeof(subj), M_("Re: %s"), pSubject);
|
||
}
|
||
else
|
||
{
|
||
strncpy(reinterpret_cast<char *>(subj),
|
||
pSubject,
|
||
sizeof(subj) - 1);
|
||
subj[sizeof(subj) - 1] = '\0';
|
||
}
|
||
|
||
do_expmail_start(player, tolist, subj);
|
||
|
||
// Quote original message if requested.
|
||
//
|
||
if (key & MAIL_QUOTE)
|
||
{
|
||
UTF8 fromname[MOD_LBUF_SIZE];
|
||
get_player_name(mp->from, fromname, sizeof(fromname));
|
||
const UTF8 *pMessage = MessageFetch(mp->number);
|
||
const char *pTime = mp->time.c_str();
|
||
|
||
UTF8 body[4 * MOD_LBUF_SIZE];
|
||
mux_sprintf(body, sizeof(body), M_("On %s, %s wrote:\r\n\r\n%s\r\n\r\n********** End of included message from %s\r\n"),
|
||
pTime,
|
||
reinterpret_cast<const char *>(fromname),
|
||
pMessage ? reinterpret_cast<const char *>(pMessage) : "",
|
||
reinterpret_cast<const char *>(fromname));
|
||
|
||
set_comp_attr(player, MOD_A_MAILMSG, body);
|
||
}
|
||
|
||
// Set M_REPLY flag.
|
||
//
|
||
UTF8 aFlags[32];
|
||
aFlags[0] = '\0';
|
||
get_comp_attr(player, MOD_A_MAILFLAGS, aFlags, sizeof(aFlags));
|
||
int iFlag = M_REPLY;
|
||
if (aFlags[0])
|
||
{
|
||
iFlag |= static_cast<int>(mux_atoi64(aFlags));
|
||
}
|
||
UTF8 flagbuf[16];
|
||
mux_sprintf(flagbuf, sizeof(flagbuf), T("%d"), iFlag);
|
||
set_comp_attr(player, MOD_A_MAILFLAGS, flagbuf);
|
||
}
|
||
|
||
void CMailMod::do_mail_proof(dbref player)
|
||
{
|
||
// Check composing state.
|
||
//
|
||
bool bComposing = false;
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
}
|
||
if (!bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No message started."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Read and display the draft (#1229: raw attr APIs for AF_INTERNAL).
|
||
//
|
||
UTF8 aTolist[MOD_LBUF_SIZE];
|
||
aTolist[0] = '\0';
|
||
UTF8 aMailMsg[MOD_LBUF_SIZE];
|
||
aMailMsg[0] = '\0';
|
||
UTF8 aMailSub[MOD_LBUF_SIZE];
|
||
aMailSub[0] = '\0';
|
||
|
||
get_comp_attr(player, MOD_A_MAILTO, aTolist, sizeof(aTolist));
|
||
get_comp_attr(player, MOD_A_MAILMSG, aMailMsg, sizeof(aMailMsg));
|
||
get_comp_attr(player, MOD_A_MAILSUB, aMailSub, sizeof(aMailSub));
|
||
|
||
std::string names = make_namelist(player, aTolist);
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: To: %s"),
|
||
names.c_str());
|
||
m_pINotify->RawNotify(player, msg);
|
||
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Subject: %s"),
|
||
reinterpret_cast<const char *>(aMailSub));
|
||
m_pINotify->RawNotify(player, msg);
|
||
|
||
m_pINotify->RawNotify(player, aMailMsg);
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_edit_msg(dbref player, const UTF8 *from, const UTF8 *to)
|
||
{
|
||
// Check composing state.
|
||
//
|
||
bool bComposing = false;
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
}
|
||
if (!bComposing)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No message in progress."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Read current message.
|
||
//
|
||
UTF8 aMailMsg[MOD_LBUF_SIZE];
|
||
aMailMsg[0] = '\0';
|
||
get_comp_attr(player, MOD_A_MAILMSG, aMailMsg, sizeof(aMailMsg));
|
||
|
||
// Simple find-and-replace (first occurrence only, matching server behavior).
|
||
//
|
||
const char *pFrom = reinterpret_cast<const char *>(from);
|
||
const char *pTo = reinterpret_cast<const char *>(to);
|
||
size_t nFrom = strlen(pFrom);
|
||
size_t nTo = strlen(pTo);
|
||
|
||
if (nFrom == 0)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Nothing to edit."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Build result with all occurrences replaced.
|
||
//
|
||
UTF8 result[MOD_LBUF_SIZE];
|
||
char *rp = reinterpret_cast<char *>(result);
|
||
const char *src = reinterpret_cast<const char *>(aMailMsg);
|
||
size_t remain = sizeof(result) - 1;
|
||
|
||
while (*src)
|
||
{
|
||
const char *found = strstr(src, pFrom);
|
||
if (found)
|
||
{
|
||
// Copy prefix.
|
||
//
|
||
size_t prefix = found - src;
|
||
if (prefix > remain)
|
||
{
|
||
prefix = remain;
|
||
}
|
||
memcpy(rp, src, prefix);
|
||
rp += prefix;
|
||
remain -= prefix;
|
||
|
||
// Copy replacement.
|
||
//
|
||
size_t rlen = nTo;
|
||
if (rlen > remain)
|
||
{
|
||
rlen = remain;
|
||
}
|
||
memcpy(rp, pTo, rlen);
|
||
rp += rlen;
|
||
remain -= rlen;
|
||
|
||
src = found + nFrom;
|
||
}
|
||
else
|
||
{
|
||
// Copy rest.
|
||
//
|
||
size_t rest = strlen(src);
|
||
if (rest > remain)
|
||
{
|
||
rest = remain;
|
||
}
|
||
memcpy(rp, src, rest);
|
||
rp += rest;
|
||
remain -= rest;
|
||
break;
|
||
}
|
||
}
|
||
*rp = '\0';
|
||
|
||
set_comp_attr(player, MOD_A_MAILMSG, result);
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("Text edited."));
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Malias helpers and commands.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
bool CMailMod::is_exp_mail(dbref player)
|
||
{
|
||
bool bWiz = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsWizard(player, &bWiz);
|
||
}
|
||
return bWiz;
|
||
}
|
||
|
||
bool CMailMod::make_canonical_alias(const UTF8 *alias, UTF8 *buf,
|
||
size_t bufsize, size_t *pnLen)
|
||
{
|
||
if (nullptr == alias || !isalpha(*alias))
|
||
{
|
||
*pnLen = 0;
|
||
return false;
|
||
}
|
||
|
||
const UTF8 *p = alias;
|
||
UTF8 *q = buf;
|
||
size_t nLeft = bufsize - 1;
|
||
|
||
while (*p && nLeft)
|
||
{
|
||
if (!isalpha(*p) && !isdigit(*p) && *p != '_')
|
||
{
|
||
break;
|
||
}
|
||
*q++ = *p++;
|
||
nLeft--;
|
||
}
|
||
*q = '\0';
|
||
*pnLen = q - buf;
|
||
return true;
|
||
}
|
||
|
||
malias_t *CMailMod::get_malias(dbref player, const UTF8 *alias, int *pnResult)
|
||
{
|
||
*pnResult = GMA_INVALIDFORM;
|
||
if (nullptr == alias)
|
||
{
|
||
return nullptr;
|
||
}
|
||
|
||
if (alias[0] == '#')
|
||
{
|
||
if (is_exp_mail(player))
|
||
{
|
||
int64_t x = mux_atoi64(alias + 1);
|
||
if (x < 0 || x >= static_cast<int>(m_malias.size()))
|
||
{
|
||
*pnResult = GMA_NOTFOUND;
|
||
return nullptr;
|
||
}
|
||
*pnResult = GMA_FOUND;
|
||
return m_malias[x].get();
|
||
}
|
||
}
|
||
else if (alias[0] == '*')
|
||
{
|
||
UTF8 canonical[SIZEOF_MALIAS];
|
||
size_t nLen;
|
||
if (make_canonical_alias(alias + 1, canonical, sizeof(canonical), &nLen))
|
||
{
|
||
for (size_t i = 0; i < m_malias.size(); i++)
|
||
{
|
||
malias_t *m = m_malias[i].get();
|
||
if ( m->owner == player
|
||
|| m->owner == 1 // GOD
|
||
|| is_exp_mail(player))
|
||
{
|
||
if (0 == strcasecmp(
|
||
reinterpret_cast<const char *>(canonical),
|
||
m->name.c_str()))
|
||
{
|
||
*pnResult = GMA_FOUND;
|
||
return m;
|
||
}
|
||
}
|
||
}
|
||
*pnResult = GMA_NOTFOUND;
|
||
}
|
||
}
|
||
|
||
if (*pnResult == GMA_INVALIDFORM)
|
||
{
|
||
if (is_exp_mail(player))
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Mail aliases must be of the form *<name> or #<num>."));
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Mail aliases must be of the form *<name>."));
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
void CMailMod::do_malias_create(dbref player, const UTF8 *alias,
|
||
const UTF8 *tolist)
|
||
{
|
||
int nResult;
|
||
get_malias(player, alias, &nResult);
|
||
|
||
if (nResult == GMA_INVALIDFORM)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: What alias do you want to create?."));
|
||
return;
|
||
}
|
||
if (nResult == GMA_FOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Mail Alias ‘%s’ already exists."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
|
||
// #1875: per-owner alias count cap.
|
||
//
|
||
size_t nOwned = 0;
|
||
for (size_t i = 0; i < m_malias.size(); i++)
|
||
{
|
||
if (m_malias[i] && m_malias[i]->owner == player)
|
||
{
|
||
nOwned++;
|
||
}
|
||
}
|
||
if (nOwned >= MAX_MALIAS_PER_OWNER)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("MAIL: You may only own %d mail aliases."),
|
||
MAX_MALIAS_PER_OWNER);
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
|
||
auto pt = std::make_unique<malias_t>();
|
||
|
||
// Parse the player list.
|
||
//
|
||
UTF8 head_buf[MOD_LBUF_SIZE];
|
||
strncpy(reinterpret_cast<char *>(head_buf),
|
||
reinterpret_cast<const char *>(tolist),
|
||
sizeof(head_buf) - 1);
|
||
head_buf[sizeof(head_buf) - 1] = '\0';
|
||
|
||
char *head = reinterpret_cast<char *>(head_buf);
|
||
int count = 0;
|
||
|
||
while (*head)
|
||
{
|
||
while (*head == ' ')
|
||
{
|
||
head++;
|
||
}
|
||
if (!*head)
|
||
{
|
||
break;
|
||
}
|
||
|
||
char *tail = head;
|
||
if (*tail == '"')
|
||
{
|
||
head++;
|
||
tail++;
|
||
while (*tail && *tail != '"')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
while (*tail && *tail != ' ')
|
||
{
|
||
tail++;
|
||
}
|
||
}
|
||
|
||
char saved = *tail;
|
||
*tail = '\0';
|
||
|
||
// Look up target.
|
||
//
|
||
dbref target = NOTHING;
|
||
if (0 == strcasecmp(head, "me"))
|
||
{
|
||
target = player;
|
||
}
|
||
else if (*head == '#')
|
||
{
|
||
target = static_cast<dbref>(mux_atoi64(reinterpret_cast<const UTF8 *>(head + 1)));
|
||
}
|
||
else if (nullptr != m_pIObjectInfo)
|
||
{
|
||
UTF8 namebuf[MOD_LBUF_SIZE + 1];
|
||
mux_sprintf(namebuf, sizeof(namebuf), T("*%s"), head);
|
||
m_pIObjectInfo->MatchThing(player, namebuf, &target);
|
||
}
|
||
|
||
bool bPlayer = false;
|
||
if (target >= 0 && nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(target, &bPlayer);
|
||
}
|
||
|
||
if (!bPlayer)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: No such player."));
|
||
}
|
||
else if (pt->list.size() >= MAX_MALIAS_MEMBERS)
|
||
{
|
||
// #1875: stop accepting more members once the list is full.
|
||
//
|
||
UTF8 capmsg[256];
|
||
mux_sprintf(capmsg, sizeof(capmsg),
|
||
M_("MAIL: Alias member limit (%d) reached; further names ignored."),
|
||
MAX_MALIAS_MEMBERS);
|
||
m_pINotify->RawNotify(player, capmsg);
|
||
*tail = saved;
|
||
break;
|
||
}
|
||
else
|
||
{
|
||
UTF8 addmsg[256];
|
||
UTF8 szName[64];
|
||
get_player_name(target, szName, sizeof(szName));
|
||
mux_sprintf(addmsg, sizeof(addmsg), M_("MAIL: %s added to alias %s"),
|
||
reinterpret_cast<const char *>(szName),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, addmsg);
|
||
pt->list.push_back(target);
|
||
count++;
|
||
}
|
||
|
||
*tail = saved;
|
||
head = tail;
|
||
if (*head == '"' || *head == ' ')
|
||
{
|
||
head++;
|
||
}
|
||
}
|
||
|
||
UTF8 canonical[SIZEOF_MALIAS];
|
||
size_t nLen;
|
||
if (!make_canonical_alias(alias + 1, canonical, sizeof(canonical), &nLen))
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Invalid mail alias."));
|
||
return;
|
||
}
|
||
|
||
pt->name = reinterpret_cast<const char *>(canonical);
|
||
pt->owner = player;
|
||
pt->desc = reinterpret_cast<const char *>(canonical);
|
||
pt->desc_width = nLen;
|
||
m_malias.push_back(std::move(pt));
|
||
sqlite_wt_sync_all_aliases();
|
||
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias set ‘%s’ defined."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
void CMailMod::do_malias_list(dbref player, const UTF8 *alias)
|
||
{
|
||
int nResult;
|
||
malias_t *m = get_malias(player, alias, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ not found."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if (nResult != GMA_FOUND)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool bGod = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsGod(m->owner, &bGod);
|
||
}
|
||
if (!is_exp_mail(player) && player != m->owner && !bGod)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Permission denied."));
|
||
return;
|
||
}
|
||
|
||
UTF8 buf[MOD_LBUF_SIZE];
|
||
UTF8 *bp = buf;
|
||
bp += mail_sprintf(bp, sizeof(buf), M_("MAIL: Alias *%s: "),
|
||
reinterpret_cast<const UTF8 *>(m->name.c_str()));
|
||
|
||
for (int i = static_cast<int>(m->list.size()) - 1; i >= 0; i--)
|
||
{
|
||
UTF8 szName[64];
|
||
get_player_name(m->list[i], szName, sizeof(szName));
|
||
size_t remain = sizeof(buf) - static_cast<size_t>(bp - buf);
|
||
if (remain < 64)
|
||
{
|
||
break;
|
||
}
|
||
if (strchr(reinterpret_cast<const char *>(szName), ' '))
|
||
{
|
||
bp += mail_sprintf(bp, remain, T("\"%s\" "), szName);
|
||
}
|
||
else
|
||
{
|
||
bp += mail_sprintf(bp, remain, T("%s "), szName);
|
||
}
|
||
}
|
||
m_pINotify->RawNotify(player, buf);
|
||
}
|
||
|
||
void CMailMod::do_malias_list_all(dbref player)
|
||
{
|
||
bool bGod = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsGod(player, &bGod);
|
||
}
|
||
|
||
bool notified = false;
|
||
for (size_t i = 0; i < m_malias.size(); i++)
|
||
{
|
||
malias_t *m = m_malias[i].get();
|
||
bool bOwnerGod = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsGod(m->owner, &bOwnerGod);
|
||
}
|
||
if (bOwnerGod || m->owner == player || bGod)
|
||
{
|
||
// Mail alias list schema (#1667 Phase 4 B1) — same as engine.
|
||
//
|
||
static const size_t kAliasNameCols = 12;
|
||
static const size_t kAliasDescCols = 40;
|
||
static const size_t kAliasOwnerCols = 15;
|
||
|
||
if (!notified)
|
||
{
|
||
UTF8 header[256];
|
||
size_t pos = 0;
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Name"), kAliasNameCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Description"), kAliasDescCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Owner"), kAliasOwnerCols);
|
||
m_pINotify->RawNotify(player, header);
|
||
notified = true;
|
||
}
|
||
|
||
UTF8 szOwner[64];
|
||
get_player_name(m->owner, szOwner, sizeof(szOwner));
|
||
|
||
UTF8 line[256];
|
||
size_t pos = 0;
|
||
pos = append_ljust_field(line, sizeof(line), pos,
|
||
reinterpret_cast<const UTF8 *>(m->name.c_str()),
|
||
kAliasNameCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos,
|
||
reinterpret_cast<const UTF8 *>(m->desc.c_str()),
|
||
kAliasDescCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos, szOwner,
|
||
kAliasOwnerCols);
|
||
m_pINotify->RawNotify(player, line);
|
||
}
|
||
}
|
||
m_pINotify->RawNotify(player, M_("***** End of Mail Aliases *****"));
|
||
}
|
||
|
||
void CMailMod::do_malias_adminlist(dbref player)
|
||
{
|
||
if (!is_exp_mail(player))
|
||
{
|
||
do_malias_list_all(player);
|
||
return;
|
||
}
|
||
|
||
// Mail alias admin list (#1667 Phase 4 B2).
|
||
//
|
||
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[256];
|
||
size_t pos = 0;
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Num"), kAliasNumCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Name"), kAliasNameCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Description"), kAliasDescCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Owner"), kAliasOwnerCols);
|
||
m_pINotify->RawNotify(player, header);
|
||
}
|
||
|
||
for (size_t i = 0; i < m_malias.size(); i++)
|
||
{
|
||
malias_t *m = m_malias[i].get();
|
||
UTF8 szOwner[64];
|
||
get_player_name(m->owner, szOwner, sizeof(szOwner));
|
||
|
||
UTF8 line[256];
|
||
size_t pos = 0;
|
||
UTF8 numbuf[8];
|
||
mux_sprintf(numbuf, sizeof(numbuf), T("%d"), static_cast<int>(i));
|
||
pos = append_ljust_field(line, sizeof(line), pos, numbuf,
|
||
kAliasNumCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos,
|
||
reinterpret_cast<const UTF8 *>(m->name.c_str()), kAliasNameCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos,
|
||
reinterpret_cast<const UTF8 *>(m->desc.c_str()), kAliasDescCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos, szOwner,
|
||
kAliasOwnerCols);
|
||
m_pINotify->RawNotify(player, line);
|
||
}
|
||
m_pINotify->RawNotify(player, M_("***** End of Mail Aliases *****"));
|
||
}
|
||
|
||
void CMailMod::do_malias_desc(dbref player, const UTF8 *alias,
|
||
const UTF8 *desc)
|
||
{
|
||
int nResult;
|
||
malias_t *m = get_malias(player, alias, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ not found."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if (nResult != GMA_FOUND)
|
||
{
|
||
return;
|
||
}
|
||
bool bOwnerGod = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsGod(m->owner, &bOwnerGod);
|
||
}
|
||
if (bOwnerGod && !is_exp_mail(player))
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Permission denied."));
|
||
return;
|
||
}
|
||
|
||
if (nullptr == desc || '\0' == *desc)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Description is not valid."));
|
||
return;
|
||
}
|
||
|
||
// Simple validation: just use the desc as-is (truncated).
|
||
//
|
||
size_t nDesc = strlen(reinterpret_cast<const char *>(desc));
|
||
if (nDesc > 40)
|
||
{
|
||
nDesc = 40;
|
||
}
|
||
m->desc.assign(reinterpret_cast<const char *>(desc), nDesc);
|
||
m->desc_width = nDesc;
|
||
sqlite_wt_sync_all_aliases();
|
||
m_pINotify->RawNotify(player, M_("MAIL: Description changed."));
|
||
}
|
||
|
||
void CMailMod::do_malias_chown(dbref player, const UTF8 *alias,
|
||
const UTF8 *owner)
|
||
{
|
||
if (!is_exp_mail(player))
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: You cannot do that!"));
|
||
return;
|
||
}
|
||
|
||
int nResult;
|
||
malias_t *m = get_malias(player, alias, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ not found."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if (nResult != GMA_FOUND)
|
||
{
|
||
return;
|
||
}
|
||
|
||
dbref target = NOTHING;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
UTF8 namebuf[256];
|
||
mux_sprintf(namebuf, sizeof(namebuf), T("*%s"), reinterpret_cast<const char *>(owner));
|
||
m_pIObjectInfo->MatchThing(player, namebuf, &target);
|
||
}
|
||
if (NOTHING == target)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: I do not see that here."));
|
||
return;
|
||
}
|
||
m->owner = target;
|
||
sqlite_wt_sync_all_aliases();
|
||
m_pINotify->RawNotify(player, M_("MAIL: Owner changed for alias."));
|
||
}
|
||
|
||
void CMailMod::do_malias_add(dbref player, const UTF8 *alias,
|
||
const UTF8 *person)
|
||
{
|
||
int nResult;
|
||
malias_t *m = get_malias(player, alias, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ not found."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if (nResult != GMA_FOUND)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool bOwnerGod = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsGod(m->owner, &bOwnerGod);
|
||
}
|
||
if (bOwnerGod && !is_exp_mail(player))
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Permission denied."));
|
||
return;
|
||
}
|
||
|
||
dbref thing = NOTHING;
|
||
if (nullptr != person && *person == '#')
|
||
{
|
||
thing = static_cast<dbref>(mux_atoi64(reinterpret_cast<const UTF8 *>(reinterpret_cast<const char *>(person)) + 1));
|
||
bool bPlayer = false;
|
||
if (thing >= 0 && nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(thing, &bPlayer);
|
||
}
|
||
if (!bPlayer)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Only players may be added."));
|
||
return;
|
||
}
|
||
}
|
||
if (NOTHING == thing && nullptr != m_pIObjectInfo)
|
||
{
|
||
UTF8 namebuf[256];
|
||
mux_sprintf(namebuf, sizeof(namebuf), T("*%s"), reinterpret_cast<const char *>(person));
|
||
m_pIObjectInfo->MatchThing(player, namebuf, &thing);
|
||
}
|
||
if (NOTHING == thing)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: I do not see that person here."));
|
||
return;
|
||
}
|
||
|
||
for (size_t i = 0; i < m->list.size(); i++)
|
||
{
|
||
if (m->list[i] == thing)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: That person is already on the list."));
|
||
return;
|
||
}
|
||
}
|
||
|
||
// #1875: per-alias member cap.
|
||
//
|
||
if (m->list.size() >= MAX_MALIAS_MEMBERS)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("MAIL: Alias ‘%s’ already has the maximum of %d members."),
|
||
m->name.c_str(), MAX_MALIAS_MEMBERS);
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
|
||
m->list.push_back(thing);
|
||
sqlite_wt_sync_all_aliases();
|
||
|
||
UTF8 szName[64];
|
||
get_player_name(thing, szName, sizeof(szName));
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: %s added to %s"),
|
||
reinterpret_cast<const char *>(szName),
|
||
m->name.c_str());
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
void CMailMod::do_malias_remove(dbref player, const UTF8 *alias,
|
||
const UTF8 *person)
|
||
{
|
||
int nResult;
|
||
malias_t *m = get_malias(player, alias, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ not found."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if (nResult != GMA_FOUND)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool bOwnerGod = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsGod(m->owner, &bOwnerGod);
|
||
}
|
||
if (bOwnerGod && !is_exp_mail(player))
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Permission denied."));
|
||
return;
|
||
}
|
||
|
||
dbref thing = NOTHING;
|
||
if (nullptr != person && *person == '#')
|
||
{
|
||
thing = static_cast<dbref>(mux_atoi64(reinterpret_cast<const UTF8 *>(reinterpret_cast<const char *>(person)) + 1));
|
||
}
|
||
if (NOTHING == thing && nullptr != m_pIObjectInfo)
|
||
{
|
||
UTF8 namebuf[256];
|
||
mux_sprintf(namebuf, sizeof(namebuf), T("*%s"), reinterpret_cast<const char *>(person));
|
||
m_pIObjectInfo->MatchThing(player, namebuf, &thing);
|
||
}
|
||
if (NOTHING == thing)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: I do not see that person here."));
|
||
return;
|
||
}
|
||
|
||
auto it = std::find(m->list.begin(), m->list.end(), thing);
|
||
bool found = (it != m->list.end());
|
||
|
||
if (found)
|
||
{
|
||
m->list.erase(it);
|
||
sqlite_wt_sync_all_aliases();
|
||
|
||
UTF8 szName[64];
|
||
get_player_name(thing, szName, sizeof(szName));
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: %s removed from alias %s."),
|
||
reinterpret_cast<const char *>(szName),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
else
|
||
{
|
||
UTF8 szName[64];
|
||
get_player_name(thing, szName, sizeof(szName));
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: %s is not a member of alias %s."),
|
||
reinterpret_cast<const char *>(szName),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_malias_rename(dbref player, const UTF8 *alias,
|
||
const UTF8 *newname)
|
||
{
|
||
int nResult;
|
||
malias_t *m = get_malias(player, newname, &nResult);
|
||
if (nResult == GMA_FOUND)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: That name already exists!"));
|
||
return;
|
||
}
|
||
if (nResult != GMA_NOTFOUND)
|
||
{
|
||
return;
|
||
}
|
||
m = get_malias(player, alias, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: I cannot find that alias!"));
|
||
return;
|
||
}
|
||
if (nResult != GMA_FOUND)
|
||
{
|
||
return;
|
||
}
|
||
if (!is_exp_mail(player) && m->owner != player)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Permission denied."));
|
||
return;
|
||
}
|
||
|
||
UTF8 canonical[SIZEOF_MALIAS];
|
||
size_t nLen;
|
||
if (make_canonical_alias(newname + 1, canonical, sizeof(canonical), &nLen))
|
||
{
|
||
m->name = reinterpret_cast<const char *>(canonical);
|
||
sqlite_wt_sync_all_aliases();
|
||
m_pINotify->RawNotify(player, M_("MAIL: Mailing Alias renamed."));
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Alias is not valid."));
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_malias_delete(dbref player, const UTF8 *alias)
|
||
{
|
||
int nResult;
|
||
malias_t *m = get_malias(player, alias, &nResult);
|
||
if (nResult == GMA_NOTFOUND)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ not found."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if (nResult != GMA_FOUND)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool done = false;
|
||
for (size_t i = 0; i < m_malias.size(); i++)
|
||
{
|
||
if (m == m_malias[i].get())
|
||
{
|
||
if (m->owner == player || is_exp_mail(player))
|
||
{
|
||
done = true;
|
||
m_pINotify->RawNotify(player, M_("MAIL: Alias Deleted."));
|
||
m_malias.erase(m_malias.begin() + i);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!done)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Alias ‘%s’ not found."),
|
||
reinterpret_cast<const char *>(alias));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
else
|
||
{
|
||
sqlite_wt_sync_all_aliases();
|
||
}
|
||
}
|
||
|
||
void CMailMod::do_malias_status(dbref player)
|
||
{
|
||
if (!is_exp_mail(player))
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Permission denied."));
|
||
return;
|
||
}
|
||
UTF8 msg[128];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Number of mail aliases defined: %d"),
|
||
static_cast<int>(m_malias.size()));
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Allocated slots %d"),
|
||
static_cast<int>(m_malias.capacity()));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
void CMailMod::do_malias_switch(dbref player, const UTF8 *a1,
|
||
const UTF8 *a2)
|
||
{
|
||
if (nullptr != a1 && '\0' != *a1)
|
||
{
|
||
if (nullptr != a2 && '\0' != *a2)
|
||
{
|
||
do_malias_create(player, a1, a2);
|
||
}
|
||
else
|
||
{
|
||
do_malias_list(player, a1);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
do_malias_list_all(player);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Command implementations.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_flags(dbref player, const 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);
|
||
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
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);
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
switch (flag)
|
||
{
|
||
case M_TAG:
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Msg #%d %s."), i,
|
||
negate ? "untagged" : "tagged");
|
||
break;
|
||
|
||
case M_CLEARED:
|
||
if (Unread(mp) && !negate)
|
||
{
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Unread Msg #%d cleared! Use @mail/unclear %d to recover."),
|
||
i, i);
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Msg #%d %s."), i,
|
||
negate ? "uncleared" : "cleared");
|
||
}
|
||
break;
|
||
|
||
case M_SAFE:
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Msg #%d %s."), i,
|
||
negate ? "marked unsafe" : "marked safe");
|
||
break;
|
||
|
||
default:
|
||
msg[0] = '\0';
|
||
break;
|
||
}
|
||
|
||
if (msg[0] != '\0')
|
||
{
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!j)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You don’t have any matching messages!"));
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Display helpers.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
UTF8 *CMailMod::status_chars(struct mail *mp)
|
||
{
|
||
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;
|
||
}
|
||
|
||
UTF8 *CMailMod::mail_list_time(const UTF8 *the_time)
|
||
{
|
||
// Format: "day mon dd hh:mm:ss yyyy" → "day mon dd hh:mm yyyy"
|
||
// Chop out ":ss"
|
||
//
|
||
thread_local UTF8 buf[32];
|
||
if (nullptr == the_time || '\0' == *the_time)
|
||
{
|
||
buf[0] = '\0';
|
||
return buf;
|
||
}
|
||
|
||
const char *p = reinterpret_cast<const char *>(the_time);
|
||
char *q = reinterpret_cast<char *>(buf);
|
||
int i;
|
||
|
||
// Copy first 16 chars (up to and including "hh:mm")
|
||
//
|
||
for (i = 0; i < 16 && *p; i++)
|
||
{
|
||
*q++ = *p++;
|
||
}
|
||
|
||
// Skip 3 chars (":ss")
|
||
//
|
||
for (i = 0; i < 3 && *p; i++)
|
||
{
|
||
p++;
|
||
}
|
||
|
||
// Copy remaining (up to 5 chars for " yyyy")
|
||
//
|
||
for (i = 0; i < 5 && *p; i++)
|
||
{
|
||
*q++ = *p++;
|
||
}
|
||
*q = '\0';
|
||
return buf;
|
||
}
|
||
|
||
void CMailMod::get_player_name(dbref who, UTF8 *buf, size_t bufsize)
|
||
{
|
||
const UTF8 *pName = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetName(who, &pName);
|
||
}
|
||
if (nullptr != pName)
|
||
{
|
||
strncpy(reinterpret_cast<char *>(buf),
|
||
reinterpret_cast<const char *>(pName), bufsize - 1);
|
||
buf[bufsize - 1] = '\0';
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(buf, bufsize, T("#%d"), who);
|
||
}
|
||
}
|
||
|
||
bool CMailMod::is_connected_visible(dbref who, dbref viewer)
|
||
{
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return false;
|
||
}
|
||
bool bConnected = false;
|
||
m_pIObjectInfo->IsConnected(who, &bConnected);
|
||
if (!bConnected)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// If 'who' is Hidden (DARK flag), only visible to See_Hidden viewers.
|
||
//
|
||
unsigned int flags = 0;
|
||
m_pIObjectInfo->GetFlags(who, 0, &flags); // FLAG_WORD1 = 0
|
||
if (flags & 0x00000040) // DARK = 0x00000040
|
||
{
|
||
bool bSeeHidden = false;
|
||
m_pIObjectInfo->SeeHidden(viewer, &bSeeHidden);
|
||
return bSeeHidden;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/list — list messages in current folder.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_list(dbref player, const UTF8 *arg1,
|
||
const UTF8 *arg2)
|
||
{
|
||
const UTF8 *msglist;
|
||
int folder = player_folder(player);
|
||
|
||
if (nullptr == arg2 || '\0' == *arg2)
|
||
{
|
||
msglist = arg1;
|
||
}
|
||
else
|
||
{
|
||
// arg1 is a folder, arg2 is the msglist.
|
||
//
|
||
if (nullptr != arg1 && isdigit(*arg1))
|
||
{
|
||
int64_t f = mux_atoi64(arg1);
|
||
if (f >= 0 && f <= MAX_FOLDERS)
|
||
{
|
||
folder = f;
|
||
}
|
||
}
|
||
msglist = arg2;
|
||
}
|
||
|
||
struct mail_selector ms;
|
||
if (!parse_msglist(msglist, &ms, player))
|
||
{
|
||
return;
|
||
}
|
||
|
||
UTF8 hdr[MOD_LBUF_SIZE];
|
||
mux_sprintf(hdr, sizeof(hdr), T("––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"–––"
|
||
" MAIL: Folder %d "
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––––"
|
||
"––––"),
|
||
folder);
|
||
m_pINotify->RawNotify(player, hdr);
|
||
|
||
int i = 0;
|
||
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (Folder(mp) == folder)
|
||
{
|
||
i++;
|
||
if (mail_match(mp, ms, i))
|
||
{
|
||
UTF8 *time = mail_list_time(
|
||
reinterpret_cast<const UTF8 *>(mp->time.c_str()));
|
||
size_t nSize = MessageFetchSize(mp->number);
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
UTF8 line[MOD_LBUF_SIZE];
|
||
format_mail_list_line_at(line, sizeof(line),
|
||
status_chars(mp), i, nSize, szFromName, time,
|
||
is_connected_visible(mp->from, player)
|
||
? T("Conn") : T(" "));
|
||
m_pINotify->RawNotify(player, line);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/read — read a mail message.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_read(dbref player, const UTF8 *arg1,
|
||
const UTF8 *arg2)
|
||
{
|
||
const UTF8 *msglist;
|
||
int folder = player_folder(player);
|
||
|
||
if (nullptr == arg2 || '\0' == *arg2)
|
||
{
|
||
msglist = arg1;
|
||
}
|
||
else
|
||
{
|
||
if (nullptr != arg1 && isdigit(*arg1))
|
||
{
|
||
int64_t f = mux_atoi64(arg1);
|
||
if (f >= 0 && f <= MAX_FOLDERS)
|
||
{
|
||
folder = f;
|
||
}
|
||
}
|
||
msglist = arg2;
|
||
}
|
||
|
||
struct mail_selector ms;
|
||
if (!parse_msglist(msglist, &ms, player))
|
||
{
|
||
return;
|
||
}
|
||
|
||
int i = 0, j = 0;
|
||
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (Folder(mp) == folder)
|
||
{
|
||
i++;
|
||
if (mail_match(mp, ms, i))
|
||
{
|
||
j++;
|
||
const UTF8 *body = MessageFetch(mp->number);
|
||
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
UTF8 hdr[MOD_LBUF_SIZE];
|
||
format_mail_detail_header(hdr, sizeof(hdr),
|
||
i, szFromName,
|
||
reinterpret_cast<const UTF8 *>(mp->time.c_str()),
|
||
is_connected_visible(mp->from, player),
|
||
folder,
|
||
Read(mp) ? T("Read") : T("Unread"),
|
||
Cleared(mp) ? " Cleared" : "",
|
||
Urgent(mp) ? " Urgent" : "",
|
||
Mass(mp) ? " Mass" : "",
|
||
Forward(mp) ? " Forward" : "",
|
||
M_Safe(mp) ? " Safe" : "",
|
||
Tagged(mp) ? " Tagged" : "",
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()),
|
||
0);
|
||
m_pINotify->RawNotify(player, hdr);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
m_pINotify->RawNotify(player, body);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
|
||
if (Unread(mp))
|
||
{
|
||
mp->read |= M_ISREAD;
|
||
sqlite_wt_update_mail_flags(mp);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!j)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You don’t have that many matching messages!"));
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/next — read the next unread message.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_next(dbref player)
|
||
{
|
||
int folder = player_folder(player);
|
||
int i = 0;
|
||
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (Folder(mp) == folder)
|
||
{
|
||
i++;
|
||
if (Unread(mp))
|
||
{
|
||
const UTF8 *body = MessageFetch(mp->number);
|
||
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
|
||
UTF8 szFromName[64];
|
||
get_player_name(mp->from, szFromName, sizeof(szFromName));
|
||
|
||
UTF8 hdr[MOD_LBUF_SIZE];
|
||
format_mail_detail_header(hdr, sizeof(hdr),
|
||
i, szFromName,
|
||
reinterpret_cast<const UTF8 *>(mp->time.c_str()),
|
||
is_connected_visible(mp->from, player),
|
||
folder,
|
||
T("Unread"),
|
||
Urgent(mp) ? " Urgent" : "",
|
||
Mass(mp) ? " Mass" : "",
|
||
Forward(mp) ? " Forward" : "",
|
||
M_Safe(mp) ? " Safe" : "",
|
||
Tagged(mp) ? " Tagged" : "",
|
||
"",
|
||
reinterpret_cast<const UTF8 *>(mp->subject.c_str()),
|
||
0);
|
||
m_pINotify->RawNotify(player, hdr);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
m_pINotify->RawNotify(player, body);
|
||
m_pINotify->RawNotify(player, MOD_DASH_LINE);
|
||
|
||
mp->read |= M_ISREAD;
|
||
sqlite_wt_update_mail_flags(mp);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You have no unread messages in that folder."));
|
||
}
|
||
|
||
void CMailMod::do_mail_purge(dbref player)
|
||
{
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto it = pList->begin(); it != pList->end(); )
|
||
{
|
||
struct mail *mp = &*it;
|
||
if (Cleared(mp))
|
||
{
|
||
sqlite_wt_delete_mail(mp);
|
||
MessageReferenceDec(mp->number);
|
||
it = pList->erase(it);
|
||
}
|
||
else
|
||
{
|
||
++it;
|
||
}
|
||
}
|
||
if (pList->empty()) m_mail_htab.erase(player);
|
||
}
|
||
m_pINotify->RawNotify(player, M_("MAIL: Mailbox purged."));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/file — move messages to a folder.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_file(dbref player, const UTF8 *msglist,
|
||
const UTF8 *folder)
|
||
{
|
||
struct mail_selector ms;
|
||
if (!parse_msglist(msglist, &ms, player))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Parse folder number.
|
||
//
|
||
int64_t foldernum = -1;
|
||
if (nullptr != folder && isdigit(*folder))
|
||
{
|
||
foldernum = mux_atoi64(folder);
|
||
if (foldernum < 0 || foldernum > MAX_FOLDERS)
|
||
{
|
||
foldernum = -1;
|
||
}
|
||
}
|
||
if (-1 == foldernum)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Invalid folder specification"));
|
||
return;
|
||
}
|
||
|
||
int i = 0, j = 0;
|
||
int origfold = player_folder(player);
|
||
|
||
std::list<mail> *pList = MailList(player);
|
||
if (pList)
|
||
{
|
||
for (auto &m : *pList)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (All(ms) || (Folder(mp) == origfold))
|
||
{
|
||
i++;
|
||
if (mail_match(mp, ms, i))
|
||
{
|
||
j++;
|
||
mp->read &= M_FMASK;
|
||
mp->read |= FolderBit(foldernum);
|
||
sqlite_wt_update_mail_flags(mp);
|
||
|
||
UTF8 msg[128];
|
||
mux_sprintf(msg, sizeof(msg), M_("MAIL: Msg %d filed in folder %d"), i, foldernum);
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!j)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: You don’t have any matching messages!"));
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @mail/stats, @mail/dstats, @mail/fstats — mail statistics.
|
||
//
|
||
// #1631: this was one invented per-player summary served identically for all
|
||
// three switches, ignoring both the [<player>] argument and the wizard gate.
|
||
// The engine (modules/engine/mail.cpp do_mail_stats) is the oracle here: three
|
||
// cumulative detail levels, an optional target, and "no target from a wizard"
|
||
// meaning the whole spool rather than the caller. Mirror it.
|
||
//
|
||
// The engine charges mudconf.searchcost via payfor() before doing the work;
|
||
// since #1654 the module does too, reading the cost and coin names through
|
||
// mux_IGameConfig at call time and charging via IObjectInfo::PayFor (which,
|
||
// like payfor(), exempts wizards). Against an engine without the interface
|
||
// the module keeps the old behaviour of not charging.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::do_mail_stats(dbref player, const UTF8 *name, int full)
|
||
{
|
||
int fc = 0, fr = 0, fu = 0, tc = 0, tr = 0, tu = 0, count = 0;
|
||
size_t cchars = 0, fchars = 0, tchars = 0;
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("MAIL: Mail statistics are unavailable."));
|
||
return;
|
||
}
|
||
|
||
bool bWizard = false;
|
||
m_pIObjectInfo->IsWizard(player, &bWizard);
|
||
|
||
// Resolve the target. bAll stands in for the engine's AMBIGUOUS: no name
|
||
// from a wizard means the whole spool rather than a particular player.
|
||
//
|
||
bool bAll = false;
|
||
dbref target = NOTHING;
|
||
if ( nullptr == name
|
||
|| '\0' == *name)
|
||
{
|
||
if (bWizard)
|
||
{
|
||
bAll = true;
|
||
}
|
||
else
|
||
{
|
||
target = player;
|
||
}
|
||
}
|
||
else if (NUMBER_TOKEN == *name)
|
||
{
|
||
target = static_cast<dbref>(strtol(
|
||
reinterpret_cast<const char *>(name) + 1, nullptr, 10));
|
||
bool bPlayer = false;
|
||
if ( MUX_FAILED(m_pIObjectInfo->IsPlayer(target, &bPlayer))
|
||
|| !bPlayer)
|
||
{
|
||
target = NOTHING;
|
||
}
|
||
}
|
||
else if (0 == strcasecmp(reinterpret_cast<const char *>(name), "me"))
|
||
{
|
||
target = player;
|
||
}
|
||
else
|
||
{
|
||
m_pIObjectInfo->LookupPlayer(player, name, true, &target);
|
||
}
|
||
|
||
if ( !bAll
|
||
&& NOTHING == target)
|
||
{
|
||
m_pIObjectInfo->MatchThing(player, name, &target);
|
||
}
|
||
if ( !bAll
|
||
&& NOTHING == target)
|
||
{
|
||
mux_sprintf(msg, sizeof(msg), M_("%s: No such player."),
|
||
(nullptr != name) ? name : T(""));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
if ( !bWizard
|
||
&& target != player)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("The post office protects privacy!"));
|
||
return;
|
||
}
|
||
|
||
// This command walks the whole database; the engine charges for it, and
|
||
// so do we (#1654). Query the cost per call -- caching at Initialize is
|
||
// #1613's bug. PayFor mirrors payfor(): wizards and Free_Money pass.
|
||
//
|
||
if ( nullptr != m_pIGameConfig
|
||
&& nullptr != m_pIObjectInfo)
|
||
{
|
||
GAME_CONFIG gc;
|
||
memset(&gc, 0, sizeof(gc));
|
||
gc.cbSize = sizeof(gc);
|
||
if (MUX_SUCCEEDED(m_pIGameConfig->GetGameConfig(&gc)))
|
||
{
|
||
bool bPaid = false;
|
||
m_pIObjectInfo->PayFor(player, gc.searchcost, &bPaid);
|
||
if (!bPaid)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Finding mail stats costs %d %s."),
|
||
gc.searchcost,
|
||
(1 == gc.searchcost) ? gc.one_coin : gc.many_coins);
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (bAll)
|
||
{
|
||
// Stats for all.
|
||
//
|
||
if (0 == full)
|
||
{
|
||
for (auto &it : m_mail_htab)
|
||
{
|
||
count += static_cast<int>(it.second.size());
|
||
}
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("There is %d message in the mail spool.",
|
||
"There are %d messages in the mail spool.", count),
|
||
count);
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
|
||
for (auto &it : m_mail_htab)
|
||
{
|
||
for (auto &m : it.second)
|
||
{
|
||
struct mail *mp = &m;
|
||
// No +1: MessageFetchSize is std::string::size() and does
|
||
// not include a NUL. The engine dropped the same phantom
|
||
// in #1639; the module's stats path still had it, so
|
||
// @mail/fstats disagreed with list sizes and with the
|
||
// engine by one byte per message (#1652).
|
||
//
|
||
if (Cleared(mp))
|
||
{
|
||
fc++;
|
||
cchars += MessageFetchSize(mp->number);
|
||
}
|
||
else if (Read(mp))
|
||
{
|
||
fr++;
|
||
fchars += MessageFetchSize(mp->number);
|
||
}
|
||
else
|
||
{
|
||
fu++;
|
||
tchars += MessageFetchSize(mp->number);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (1 == full)
|
||
{
|
||
mux_sprintf(msg, sizeof(msg),
|
||
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);
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
|
||
mux_sprintf(msg, sizeof(msg),
|
||
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, static_cast<int>(fchars));
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
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, static_cast<int>(tchars));
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
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, static_cast<int>(cchars));
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
|
||
// Individual stats. Mail *to* the target lives in the target's own list,
|
||
// but the "sent by target" side needs the full sweep.
|
||
//
|
||
const UTF8 *pMoniker = nullptr;
|
||
m_pIObjectInfo->GetMoniker(target, &pMoniker);
|
||
if (nullptr == pMoniker)
|
||
{
|
||
pMoniker = T("");
|
||
}
|
||
|
||
if (0 == full)
|
||
{
|
||
for (auto &it : m_mail_htab)
|
||
{
|
||
for (auto &m : it.second)
|
||
{
|
||
if (m.from == target)
|
||
{
|
||
fr++;
|
||
}
|
||
if (m.to == target)
|
||
{
|
||
tr++;
|
||
}
|
||
}
|
||
}
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("%s sent %d message.", "%s sent %d messages.", fr),
|
||
pMoniker, fr);
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("%s has %d message.", "%s has %d messages.", tr),
|
||
pMoniker, tr);
|
||
m_pINotify->RawNotify(player, msg);
|
||
return;
|
||
}
|
||
|
||
// More detailed message count.
|
||
//
|
||
UTF8 last[50];
|
||
last[0] = '\0';
|
||
for (auto &it : m_mail_htab)
|
||
{
|
||
for (auto &m : it.second)
|
||
{
|
||
struct mail *mp = &m;
|
||
if (mp->from == target)
|
||
{
|
||
if (Cleared(mp))
|
||
{
|
||
fc++;
|
||
}
|
||
else if (Read(mp))
|
||
{
|
||
fr++;
|
||
}
|
||
else
|
||
{
|
||
fu++;
|
||
}
|
||
if (2 == full)
|
||
{
|
||
fchars += MessageFetchSize(mp->number);
|
||
}
|
||
}
|
||
if (mp->to == target)
|
||
{
|
||
if ( !tr
|
||
&& !tu)
|
||
{
|
||
mux_strncpy(last, reinterpret_cast<const UTF8 *>(
|
||
mp->time.c_str()), sizeof(last) - 1);
|
||
}
|
||
if (Cleared(mp))
|
||
{
|
||
tc++;
|
||
}
|
||
else if (Read(mp))
|
||
{
|
||
tr++;
|
||
}
|
||
else
|
||
{
|
||
tu++;
|
||
}
|
||
if (2 == full)
|
||
{
|
||
tchars += MessageFetchSize(mp->number);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
mux_sprintf(msg, sizeof(msg), M_("Mail statistics for %s:"), pMoniker);
|
||
m_pINotify->RawNotify(player, msg);
|
||
|
||
if (1 == full)
|
||
{
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("%d message sent, %d unread, %d cleared.",
|
||
"%d messages sent, %d unread, %d cleared.",
|
||
fr + fu + fc),
|
||
fc + fr + fu, fu, fc);
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("%d message received, %d unread, %d cleared.",
|
||
"%d messages received, %d unread, %d cleared.",
|
||
tr + tu + tc),
|
||
tc + tr + tu, tu, tc);
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(msg, sizeof(msg),
|
||
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, static_cast<int>(fchars));
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
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, static_cast<int>(tchars));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
if (0 < tc + tr + tu)
|
||
{
|
||
mux_sprintf(msg, sizeof(msg), M_("Last is dated %s"), last);
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Default @mail handler — routes to list, read, clear, purge, or send.
|
||
// Returns true if handled, false if sending (unimplemented).
|
||
// ---------------------------------------------------------------------------
|
||
|
||
bool CMailMod::do_mail_stub(dbref player, const UTF8 *arg1,
|
||
const UTF8 *arg2)
|
||
{
|
||
if (nullptr == arg1 || '\0' == *arg1)
|
||
{
|
||
if (arg2 && *arg2)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("MAIL: Invalid mail command."));
|
||
return true;
|
||
}
|
||
do_mail_list(player, arg1, nullptr);
|
||
return true;
|
||
}
|
||
|
||
if (0 == strcasecmp(reinterpret_cast<const char *>(arg1), "purge"))
|
||
{
|
||
do_mail_purge(player);
|
||
return true;
|
||
}
|
||
|
||
if (0 == strcasecmp(reinterpret_cast<const char *>(arg1), "clear"))
|
||
{
|
||
do_mail_flags(player, arg2, M_CLEARED, false);
|
||
return true;
|
||
}
|
||
|
||
if (0 == strcasecmp(reinterpret_cast<const char *>(arg1), "unclear"))
|
||
{
|
||
do_mail_flags(player, arg2, M_CLEARED, true);
|
||
return true;
|
||
}
|
||
|
||
if (nullptr != arg2 && '\0' != *arg2)
|
||
{
|
||
// #1198: composition start lives in the module store — do not
|
||
// return NOTIMPLEMENTED and let the engine fall through.
|
||
//
|
||
do_expmail_start(player, arg1, arg2);
|
||
return true;
|
||
}
|
||
|
||
// Must be reading or listing mail.
|
||
//
|
||
if (isdigit(*arg1) && nullptr == strchr(
|
||
reinterpret_cast<const char *>(arg1), '-'))
|
||
{
|
||
do_mail_read(player, arg1, nullptr);
|
||
}
|
||
else
|
||
{
|
||
do_mail_list(player, arg1, nullptr);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// mux_IMailControl implementation.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CMailMod::PlayerConnect(dbref player)
|
||
{
|
||
CheckMail(player, player_folder(player), false);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CMailMod::PlayerNuke(dbref player)
|
||
{
|
||
return DestroyPlayerMail(player);
|
||
}
|
||
|
||
MUX_RESULT CMailMod::MailCommand(dbref executor, int key,
|
||
const UTF8 *pArg1, const UTF8 *pArg2)
|
||
{
|
||
switch (key & ~MAIL_QUOTE)
|
||
{
|
||
case MAIL_CLEAR:
|
||
do_mail_flags(executor, pArg1, M_CLEARED, false);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_UNCLEAR:
|
||
do_mail_flags(executor, pArg1, M_CLEARED, true);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_TAG:
|
||
do_mail_flags(executor, pArg1, M_TAG, false);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_UNTAG:
|
||
do_mail_flags(executor, pArg1, M_TAG, true);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_SAFE:
|
||
do_mail_flags(executor, pArg1, M_SAFE, false);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_UNSAFE:
|
||
do_mail_flags(executor, pArg1, M_SAFE, true);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_PURGE:
|
||
do_mail_purge(executor);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_LIST:
|
||
do_mail_list(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_READ:
|
||
do_mail_read(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_NEXT:
|
||
do_mail_next(executor);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_FILE:
|
||
do_mail_file(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_STATS:
|
||
do_mail_stats(executor, pArg1, 0);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_DSTATS:
|
||
do_mail_stats(executor, pArg1, 1);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_FSTATS:
|
||
do_mail_stats(executor, pArg1, 2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_REVIEW:
|
||
do_mail_review(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_FOLDER:
|
||
do_mail_change_folder(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_RETRACT:
|
||
do_mail_retract(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_NUKE:
|
||
do_mail_nuke(executor);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_SEND:
|
||
do_expmail_stop(executor, 0);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_QUICK:
|
||
do_mail_quick(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_CC:
|
||
// #1196: composition add, not one-shot send.
|
||
//
|
||
do_mail_cc(executor, pArg1, false);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_BCC:
|
||
// #1196: composition add with blind numlist prefix.
|
||
//
|
||
do_mail_cc(executor, pArg1, true);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_PROOF:
|
||
do_mail_proof(executor);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_ABORT:
|
||
do_expmail_abort(executor);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_FORWARD:
|
||
do_mail_fwd(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_REPLY:
|
||
do_mail_reply(executor, pArg1, false, key);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_REPLYALL:
|
||
do_mail_reply(executor, pArg1, true, key);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_URGENT:
|
||
do_expmail_stop(executor, M_URGENT);
|
||
return MUX_S_OK;
|
||
|
||
case MAIL_EDIT:
|
||
do_edit_msg(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case 0:
|
||
// Default @mail (no switch).
|
||
//
|
||
if (do_mail_stub(executor, pArg1, pArg2))
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
return MUX_E_NOTIMPLEMENTED;
|
||
|
||
default:
|
||
return MUX_E_NOTIMPLEMENTED;
|
||
}
|
||
}
|
||
|
||
MUX_RESULT CMailMod::MaliasCommand(dbref executor, int key,
|
||
const UTF8 *pArg1, const UTF8 *pArg2)
|
||
{
|
||
switch (key)
|
||
{
|
||
case 0:
|
||
do_malias_switch(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_DESC:
|
||
do_malias_desc(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_CHOWN:
|
||
do_malias_chown(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_ADD:
|
||
do_malias_add(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_REMOVE:
|
||
do_malias_remove(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_DELETE:
|
||
do_malias_delete(executor, pArg1);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_RENAME:
|
||
do_malias_rename(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_LIST:
|
||
do_malias_adminlist(executor);
|
||
return MUX_S_OK;
|
||
|
||
case MALIAS_STATUS:
|
||
do_malias_status(executor);
|
||
return MUX_S_OK;
|
||
|
||
default:
|
||
return MUX_E_NOTIMPLEMENTED;
|
||
}
|
||
}
|
||
|
||
MUX_RESULT CMailMod::FolderCommand(dbref executor, int key, int nargs,
|
||
const UTF8 *pArg1, const UTF8 *pArg2)
|
||
{
|
||
switch (key)
|
||
{
|
||
case FOLDER_FILE:
|
||
do_mail_file(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case FOLDER_LIST:
|
||
ListMailInFolder(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case FOLDER_READ:
|
||
do_mail_read(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
case FOLDER_SET:
|
||
do_mail_change_folder(executor, pArg1, pArg2);
|
||
return MUX_S_OK;
|
||
|
||
default:
|
||
if (nullptr == pArg1 || '\0' == *pArg1)
|
||
{
|
||
DoListMailBrief(executor);
|
||
}
|
||
else if (2 == nargs)
|
||
{
|
||
do_mail_read(executor, pArg1, pArg2);
|
||
}
|
||
else
|
||
{
|
||
do_mail_change_folder(executor, pArg1, pArg2);
|
||
}
|
||
return MUX_S_OK;
|
||
}
|
||
}
|
||
|
||
MUX_RESULT CMailMod::CheckMail(dbref player, int folder, bool silent)
|
||
{
|
||
int rc, uc, cc, gc;
|
||
count_mail_internal(player, folder, &rc, &uc, &cc);
|
||
urgent_mail_internal(player, folder, &gc);
|
||
|
||
if (rc + uc > 0)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("MAIL: %d message in folder %d [%s].",
|
||
"MAIL: %d messages in folder %d [%s].", rc + uc),
|
||
rc + uc, folder,
|
||
reinterpret_cast<const char *>(get_folder_name(player, folder)));
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("MAIL: %d unread message.",
|
||
"MAIL: %d unread messages.", uc), uc);
|
||
m_pINotify->RawNotify(player, msg);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
MN_("MAIL: %d cleared message.",
|
||
"MAIL: %d cleared messages.", cc), cc);
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
else if (!silent)
|
||
{
|
||
// Blank lines stay bare; prose is M_ without embedded CR (#1443 / #1419).
|
||
// Matches engine do_mail_count.
|
||
//
|
||
m_pINotify->RawNotify(player, T(""));
|
||
m_pINotify->RawNotify(player, M_("MAIL: You have no mail."));
|
||
m_pINotify->RawNotify(player, T(""));
|
||
}
|
||
if (gc > 0)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg), 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,
|
||
reinterpret_cast<const char *>(get_folder_name(player, folder)));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CMailMod::ExpireMail(void)
|
||
{
|
||
// #1196: align with engine check_mail_expiration — negative/zero never
|
||
// expire; age in seconds; skip M_Safe; expire unread; wizard exempt.
|
||
// (Engine also exempts No_Mail_Expire power holders; COM exposes only
|
||
// IsWizard today.)
|
||
//
|
||
if (m_mail_expiration <= 0)
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
CLinearTimeAbsolute ltaNow;
|
||
ltaNow.GetLocal();
|
||
const int expire_secs = m_mail_expiration * 86400;
|
||
|
||
for (auto &kv : m_mail_htab)
|
||
{
|
||
dbref player = kv.first;
|
||
|
||
bool bWizard = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsWizard(player, &bWizard);
|
||
}
|
||
if (bWizard)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
auto &mlist = kv.second;
|
||
for (auto it = mlist.begin(); it != mlist.end(); )
|
||
{
|
||
// @mail/safe messages never expire.
|
||
//
|
||
if (it->read & M_SAFE)
|
||
{
|
||
++it;
|
||
continue;
|
||
}
|
||
|
||
CLinearTimeAbsolute ltaMail;
|
||
if (!ltaMail.SetString(reinterpret_cast<const UTF8 *>(it->time.c_str())))
|
||
{
|
||
++it;
|
||
continue;
|
||
}
|
||
|
||
CLinearTimeDelta ltd(ltaMail, ltaNow);
|
||
if (ltd.ReturnSeconds() <= expire_secs)
|
||
{
|
||
++it;
|
||
continue;
|
||
}
|
||
|
||
MessageReferenceDec(it->number);
|
||
sqlite_wt_delete_mail(&(*it));
|
||
it = mlist.erase(it);
|
||
}
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CMailMod::CountMail(dbref player, int folder,
|
||
int *pRead, int *pUnread, int *pCleared)
|
||
{
|
||
count_mail_internal(player, folder, pRead, pUnread, pCleared);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CMailMod::DestroyPlayerMail(dbref player)
|
||
{
|
||
// Step 1: Purge received mail.
|
||
//
|
||
MailListRemoveAll(player);
|
||
sqlite_wt_delete_all_mail(player);
|
||
|
||
// Step 2: Orphan sent mail in every other player's mailbox.
|
||
//
|
||
for (auto &kv : m_mail_htab)
|
||
{
|
||
for (auto &m : kv.second)
|
||
{
|
||
if (m.from == player)
|
||
{
|
||
m.from = NOTHING;
|
||
sqlite_wt_update_mail_flags(&m);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Step 3: Clean up mail aliases.
|
||
//
|
||
for (size_t i = 0; i < m_malias.size(); i++)
|
||
{
|
||
if (m_malias[i])
|
||
{
|
||
m_malias[i]->list.erase(
|
||
std::remove(m_malias[i]->list.begin(),
|
||
m_malias[i]->list.end(), player),
|
||
m_malias[i]->list.end());
|
||
}
|
||
}
|
||
sqlite_wt_sync_all_aliases();
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CMailMod::GetRevision(int *pRev)
|
||
{
|
||
if (nullptr == pRev)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
*pRev = m_revision;
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Softcode mailsend() entry — mirrors engine do_mail_send_softcode against
|
||
// the module-owned store (#1191).
|
||
//
|
||
MUX_RESULT CMailMod::SoftcodeSend(dbref player, const UTF8 *recipients,
|
||
const UTF8 *subject, const UTF8 *message, const UTF8 **ppError)
|
||
{
|
||
if (nullptr == ppError)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
*ppError = nullptr;
|
||
|
||
if (player < 0)
|
||
{
|
||
*ppError = S_("#-1 NOT A PLAYER");
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
if (nullptr == recipients || '\0' == recipients[0])
|
||
{
|
||
*ppError = S_("#-1 NO RECIPIENTS");
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
if (nullptr == subject || '\0' == subject[0])
|
||
{
|
||
*ppError = S_("#-1 NO SUBJECT");
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
if (nullptr == message || '\0' == message[0])
|
||
{
|
||
*ppError = S_("#-1 NO MESSAGE");
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
// Resolve owner if needed — softcode may run on a non-player object.
|
||
// IMailDelivery / ObjectInfo tell us if player is a player.
|
||
//
|
||
bool bIsPlayer = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(player, &bIsPlayer);
|
||
}
|
||
if (!bIsPlayer && nullptr != m_pIObjectInfo)
|
||
{
|
||
dbref owner = -1;
|
||
m_pIObjectInfo->GetOwner(player, &owner);
|
||
player = owner;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(player, &bIsPlayer);
|
||
}
|
||
if (!bIsPlayer)
|
||
{
|
||
*ppError = S_("#-1 NOT A PLAYER");
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
}
|
||
|
||
if (nullptr != m_pIMailDelivery)
|
||
{
|
||
bool bComposing = false;
|
||
m_pIMailDelivery->IsComposing(player, &bComposing);
|
||
if (bComposing)
|
||
{
|
||
*ppError = S_("#-1 MAIL ALREADY IN PROGRESS");
|
||
return MUX_E_FAIL;
|
||
}
|
||
bool bWiz = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->IsWizard(player, &bWiz);
|
||
}
|
||
if (!bWiz)
|
||
{
|
||
bool bThrottled = false;
|
||
m_pIMailDelivery->ThrottleCheck(player, &bThrottled);
|
||
if (bThrottled)
|
||
{
|
||
*ppError = S_("#-1 TOO MUCH MAIL SENT");
|
||
return MUX_E_FAIL;
|
||
}
|
||
}
|
||
}
|
||
|
||
std::string numlist = make_numlist(player, recipients, false);
|
||
if (numlist.empty())
|
||
{
|
||
*ppError = S_("#-1 NO VALID RECIPIENTS");
|
||
return MUX_E_FAIL;
|
||
}
|
||
|
||
// mail_to_list takes ownership of a malloc'd buffer.
|
||
//
|
||
size_t nLen = numlist.size() + 1;
|
||
UTF8 *numlist_copy = static_cast<UTF8 *>(malloc(nLen));
|
||
if (nullptr == numlist_copy)
|
||
{
|
||
*ppError = S_("#-1 OUT OF MEMORY");
|
||
return MUX_E_OUTOFMEMORY;
|
||
}
|
||
memcpy(numlist_copy, numlist.c_str(), nLen);
|
||
|
||
mail_to_list(player, numlist_copy, subject, message, 0, true);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// mux_IServerEventsSink implementation.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CMailMod::startup(void)
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("MAIL"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module: startup event."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
void CMailMod::presync_database(void)
|
||
{
|
||
// Nothing needed — SQLite write-through handles persistence.
|
||
}
|
||
|
||
void CMailMod::presync_database_sigsegv(void)
|
||
{
|
||
// Nothing.
|
||
}
|
||
|
||
void CMailMod::dump_database(int dump_type)
|
||
{
|
||
UNUSED_PARAMETER(dump_type);
|
||
// Nothing needed — SQLite WAL checkpoint handles durability.
|
||
}
|
||
|
||
void CMailMod::dump_complete_signal(void)
|
||
{
|
||
// Nothing.
|
||
}
|
||
|
||
void CMailMod::shutdown(void)
|
||
{
|
||
// Release storage interface. Null the member field *before* calling
|
||
// Release() so that any concurrent caller which re-reads m_pIStorage
|
||
// during the release (in a future multi-threaded evaluator) sees the
|
||
// null and bails, instead of racing with the decrement and possibly
|
||
// triggering a double Release.
|
||
//
|
||
mux_IMailStorage *pStorage = m_pIStorage;
|
||
m_pIStorage = nullptr;
|
||
if (nullptr != pStorage)
|
||
{
|
||
pStorage->Release();
|
||
}
|
||
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("MAIL"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Mail module: shutdown event."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
void CMailMod::dbck(void)
|
||
{
|
||
// Mail consistency check: remove mail referencing destroyed players.
|
||
//
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (auto it = m_mail_htab.begin(); it != m_mail_htab.end(); )
|
||
{
|
||
bool bValid = false;
|
||
m_pIObjectInfo->IsValid(it->first, &bValid);
|
||
if (!bValid)
|
||
{
|
||
// Recipient destroyed — purge their mail.
|
||
//
|
||
for (auto &m : it->second)
|
||
{
|
||
MessageReferenceDec(m.number);
|
||
}
|
||
sqlite_wt_delete_all_mail(it->first);
|
||
it = m_mail_htab.erase(it);
|
||
}
|
||
else
|
||
{
|
||
// Check for mail from destroyed senders — mark as NOTHING.
|
||
//
|
||
for (auto &m : it->second)
|
||
{
|
||
if (m.from != NOTHING)
|
||
{
|
||
bool bFromValid = false;
|
||
m_pIObjectInfo->IsValid(m.from, &bFromValid);
|
||
if (!bFromValid)
|
||
{
|
||
m.from = NOTHING;
|
||
sqlite_wt_update_mail_flags(&m);
|
||
}
|
||
}
|
||
}
|
||
++it;
|
||
}
|
||
}
|
||
}
|
||
|
||
void CMailMod::connect(dbref player, int isnew, int num)
|
||
{
|
||
UNUSED_PARAMETER(isnew);
|
||
UNUSED_PARAMETER(num);
|
||
PlayerConnect(player);
|
||
}
|
||
|
||
void CMailMod::disconnect(dbref player, int num)
|
||
{
|
||
UNUSED_PARAMETER(player);
|
||
UNUSED_PARAMETER(num);
|
||
}
|
||
|
||
void CMailMod::data_create(dbref object)
|
||
{
|
||
UNUSED_PARAMETER(object);
|
||
}
|
||
|
||
void CMailMod::data_clone(dbref clone, dbref source)
|
||
{
|
||
UNUSED_PARAMETER(clone);
|
||
UNUSED_PARAMETER(source);
|
||
}
|
||
|
||
void CMailMod::data_free(dbref object)
|
||
{
|
||
// When a player is destroyed, purge their mail and clean up
|
||
// references from other players' mailboxes.
|
||
//
|
||
DestroyPlayerMail(object);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CMailModFactory — boilerplate.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
CMailModFactory::CMailModFactory(void) : m_cRef(1)
|
||
{
|
||
}
|
||
|
||
CMailModFactory::~CMailModFactory()
|
||
{
|
||
}
|
||
|
||
MUX_RESULT CMailModFactory::QueryInterface(MUX_IID iid, void **ppv)
|
||
{
|
||
if (mux_IID_IUnknown == iid)
|
||
{
|
||
*ppv = static_cast<mux_IClassFactory *>(this);
|
||
}
|
||
else if (mux_IID_IClassFactory == iid)
|
||
{
|
||
*ppv = static_cast<mux_IClassFactory *>(this);
|
||
}
|
||
else
|
||
{
|
||
*ppv = nullptr;
|
||
return MUX_E_NOINTERFACE;
|
||
}
|
||
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
uint32_t CMailModFactory::AddRef(void)
|
||
{
|
||
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
|
||
}
|
||
|
||
uint32_t CMailModFactory::Release(void)
|
||
{
|
||
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
|
||
if (1 == prev)
|
||
{
|
||
delete this;
|
||
return 0;
|
||
}
|
||
return prev - 1;
|
||
}
|
||
|
||
MUX_RESULT CMailModFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
|
||
MUX_IID iid, void **ppv)
|
||
{
|
||
UNUSED_PARAMETER(pUnknownOuter);
|
||
|
||
CMailMod *pMailMod = nullptr;
|
||
try
|
||
{
|
||
pMailMod = new CMailMod;
|
||
}
|
||
catch (...)
|
||
{
|
||
; // Nothing.
|
||
}
|
||
|
||
if (nullptr == pMailMod)
|
||
{
|
||
return MUX_E_OUTOFMEMORY;
|
||
}
|
||
|
||
MUX_RESULT mr = pMailMod->FinalConstruct();
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
pMailMod->Release();
|
||
return mr;
|
||
}
|
||
|
||
mr = pMailMod->QueryInterface(iid, ppv);
|
||
pMailMod->Release();
|
||
return mr;
|
||
}
|
||
|
||
MUX_RESULT CMailModFactory::LockServer(bool bLock)
|
||
{
|
||
if (bLock)
|
||
{
|
||
g_cServerLocks++;
|
||
}
|
||
else
|
||
{
|
||
g_cServerLocks--;
|
||
}
|
||
return MUX_S_OK;
|
||
}
|