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