Skip to content
Snippets Groups Projects
postmsg.cpp 23.9 KiB
Newer Older
/* Synchronet user create/post public message routine */

/****************************************************************************
 * @format.tab-size 4		(Plain Text/Source Code File Header)			*
 * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
 *																			*
rswindell's avatar
rswindell committed
 * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
 *																			*
 * This program is free software; you can redistribute it and/or			*
 * modify it under the terms of the GNU General Public License				*
 * as published by the Free Software Foundation; either version 2			*
 * of the License, or (at your option) any later version.					*
 * See the GNU General Public License for more details: gpl.txt or			*
 * http://www.fsf.org/copyleft/gpl.html										*
 *																			*
 * For Synchronet coding style and modification guidelines, see				*
 * http://www.synchro.net/source.html										*
 *																			*
 * Note: If this box doesn't appear square, then you need to fix your tabs.	*
 ****************************************************************************/

#include "sbbs.h"
int msgbase_open(scfg_t* cfg, smb_t* smb, unsigned int subnum, int* storage, int* dupechk_hashes, uint16_t* xlat)
	*dupechk_hashes = SMB_HASH_SOURCE_DUPE;
	*xlat = XLAT_NONE;
	if ((i = smb_open_sub(cfg, smb, subnum)) != SMB_SUCCESS)
	if (smb->subnum == INVALID_SUB) {
		/* duplicate message-IDs must be allowed in mail database */
		*dupechk_hashes &= ~(1 << SMB_HASH_SOURCE_MSG_ID);
		if (cfg->sub[smb->subnum]->misc & SUB_LZH)
			*xlat = XLAT_LZH;
	if (smb->status.max_crcs == 0) /* no CRC checking means no body text dupe checking */
		*dupechk_hashes &= ~(1 << SMB_HASH_SOURCE_BODY);
	*storage = smb_storage_mode(cfg, smb);
static uchar* findsig(char* msgbuf)
{
	char* tail = strstr(msgbuf, "\n-- \r\n");
	if (tail != NULL) {
		*tail = '\0';
		tail++;
		truncsp(msgbuf);
	}
	return (uchar*)tail;
}
/****************************************************************************/
/* Posts a message on sub-board number 'subnum'								*/
/* Returns true if posted, false if not.                                    */
/****************************************************************************/
bool sbbs_t::postmsg(int subnum, int wm_mode, smb_t* resmb, smbmsg_t* remsg)
	char        str[256];
	char        title[LEN_TITLE + 1] = "";
	char        org_title[LEN_TITLE + 1] = "";
	char        top[256] = "";
	char        touser[64] = "";
	char        from[64];
	char        tags[64] = "";
	const char* editor = NULL;
	const char* charset = NULL;
	char*       msgbuf = NULL;
	uint16_t    xlat;
	ushort      msgattr = 0;
	int         i, storage;
	int         dupechk_hashes;
	int         length;
	FILE*       fp;
	smbmsg_t    msg;
	uint        reason;
	str_list_t  names = NULL;
	if (!user_can_post(&cfg, subnum, &useron, &client, &reason)) {
Rob Swindell's avatar
Rob Swindell committed
		SAFECOPY_UTF8(title, msghdr_field(remsg, remsg->subj, NULL, term_supports(UTF8)));
		if (remsg->hdr.attr & MSG_ANONYMOUS)
			SAFECOPY(from, text[Anonymous]);
Rob Swindell's avatar
Rob Swindell committed
			SAFECOPY_UTF8(from, msghdr_field(remsg, remsg->from, NULL, term_supports(UTF8)));
		// If user posted this message, reply to the original recipient again
		if (remsg->to != NULL
		    && ((remsg->from_ext != NULL && atoi(remsg->from_ext) == useron.number)
		        || stricmp(useron.alias, remsg->from) == 0 || stricmp(useron.name, remsg->from) == 0))
Rob Swindell's avatar
Rob Swindell committed
			SAFECOPY_UTF8(touser, msghdr_field(remsg, remsg->to, NULL, term_supports(UTF8)));
			SAFECOPY_UTF8(touser, from);
		if (remsg->to != NULL)
		msgattr = (ushort)(remsg->hdr.attr & MSG_PRIVATE);
		SAFECOPY(top, format_text(RegardingByToOn, remsg
		                          , title
		                          , from
		                          , msghdr_field(remsg, remsg->to, NULL, term_supports(UTF8))
		                          , timestr(smb_time(remsg->hdr.when_written))
		                          , smb_zonestr(remsg->hdr.when_written.zone, NULL)));
		if (remsg->tags != NULL)
	bprintf(text[Posting], cfg.grp[cfg.sub[subnum]->grp]->sname, cfg.sub[subnum]->lname);
	action = NODE_PMSG;
	if (!(msgattr & MSG_PRIVATE) && (cfg.sub[subnum]->misc & SUB_PONLY
	                                 || (cfg.sub[subnum]->misc & SUB_PRIV && !noyes(text[PrivatePostQ]))))
		msgattr |= MSG_PRIVATE;
	if (sys_status & SS_ABORT) {
	if (
#if 0   /* we *do* support internet posts to specific people July-11-2002 */
		!(cfg.sub[subnum]->misc & SUB_INET) &&    // Prompt for TO: user
		(cfg.sub[subnum]->misc & SUB_TOUSER || msgattr & MSG_PRIVATE || touser[0])) {
		if (!touser[0] && !(msgattr & MSG_PRIVATE))
			SAFECOPY(touser, "All");
		bputs(text[PostTo]);
		i = LEN_ALIAS;
		if (cfg.sub[subnum]->misc & SUB_QNET)
			i = 25;
		if (cfg.sub[subnum]->misc & SUB_FIDO)
			i = FIDO_NAME_LEN - 1;
		if (cfg.sub[subnum]->misc & (SUB_PNET | SUB_INET))
			i = 60;
		getstr(touser, i, K_LINE | K_EDIT | K_AUTODEL | K_TRIM, names);
		if (stricmp(touser, "ALL")
		    && !(cfg.sub[subnum]->misc & (SUB_PNET | SUB_FIDO | SUB_QNET | SUB_INET | SUB_ANON))) {
			if (cfg.sub[subnum]->misc & SUB_NAME) {
				if (!finduserstr(useron.number, USER_NAME, touser)) {
					bputs(text[UnknownUser]);
				if ((i = finduser(touser)) == 0) {
				username(&cfg, i, touser);
			}
		if (sys_status & SS_ABORT) {
	if (!touser[0])
		SAFECOPY(touser, "All");      // Default to ALL
	if (!stricmp(touser, "SYSOP") && !SYSOP)  // Change SYSOP to user #1
		username(&cfg, 1, touser);
	if (msgattr & MSG_PRIVATE && !stricmp(touser, "ALL")) {
		bputs(text[NoToUser]);
	if (msgattr & MSG_PRIVATE)
		wm_mode |= WM_PRIVATE;
	if (cfg.sub[subnum]->misc & SUB_AONLY
	    || (cfg.sub[subnum]->misc & SUB_ANON && useron.exempt & FLAG('A')
	        && !noyes(text[AnonymousQ]))) {
		msgattr |= MSG_ANONYMOUS;
		wm_mode |= WM_ANON;
	if (cfg.sub[subnum]->mod_ar[0] && chk_ar(cfg.sub[subnum]->mod_ar, &useron, &client))
		msgattr |= MSG_MODERATED;
	if (cfg.sub[subnum]->misc & SUB_SYSPERM && sub_op(subnum))
		msgattr |= MSG_PERMANENT;
	if (msgattr & MSG_PRIVATE)
		bputs(text[PostingPrivately]);

	if (msgattr & MSG_ANONYMOUS)
		bputs(text[PostingAnonymously]);
	else if (cfg.sub[subnum]->misc & SUB_NAME)
		bputs(text[UsingRealName]);

	msg_tmp_fname(useron.xedit, str, sizeof(str));
	if ((i = smb_stack(&smb, SMB_STACK_PUSH)) != SMB_SUCCESS) {
		errormsg(WHERE, ERR_OPEN, cfg.sub[subnum]->code, i, smb.last_error);
	if ((i = msgbase_open(&cfg, &smb, subnum, &storage, &dupechk_hashes, &xlat)) != SMB_SUCCESS) {
		errormsg(WHERE, ERR_OPEN, smb.file, i, smb.last_error);
		smb_stack(&smb, SMB_STACK_POP);
	if (remsg != NULL && resmb != NULL && !(wm_mode & WM_QUOTE)) {
		if (quotemsg(resmb, remsg))
	if (!writemsg(str, top, title, wm_mode, subnum, touser
	              , /* from: */ cfg.sub[subnum]->misc & SUB_NAME ? useron.name : useron.alias
	              , &editor, &charset)
	    || (length = (int)flength(str)) < 1) {  /* Bugfix Aug-20-2003: Reject negative length */
		bputs(text[Aborted]);
		smb_close(&smb);
		smb_stack(&smb, SMB_STACK_POP);
	if ((cfg.sub[subnum]->misc & SUB_MSGTAGS)
	    && (tags[0] || text[TagMessageQ][0] == 0 || !noyes(text[TagMessageQ]))) {
		bputs(text[TagMessagePrompt]);
		getstr(tags, sizeof(tags) - 1, K_EDIT | K_LINE | K_TRIM);
	if ((i = smb_locksmbhdr(&smb)) != SMB_SUCCESS) {
		smb_close(&smb);
		errormsg(WHERE, ERR_LOCK, smb.file, i, smb.last_error);
		smb_stack(&smb, SMB_STACK_POP);
	if ((i = smb_getstatus(&smb)) != SMB_SUCCESS) {
		smb_close(&smb);
		errormsg(WHERE, ERR_READ, smb.file, i, smb.last_error);
		smb_stack(&smb, SMB_STACK_POP);
	if ((msgbuf = (char*)calloc(length + 1, sizeof(char))) == NULL) {
		smb_close(&smb);
		errormsg(WHERE, ERR_ALLOC, "msgbuf", length + 1);
		smb_stack(&smb, SMB_STACK_POP);
	if ((fp = fopen(str, "rb")) == NULL) {
		smb_close(&smb);
		errormsg(WHERE, ERR_OPEN, str, O_RDONLY | O_BINARY);
		smb_stack(&smb, SMB_STACK_POP);
	i = fread(msgbuf, 1, length, fp);
		free(msgbuf);
		smb_close(&smb);
		errormsg(WHERE, ERR_READ, str, length);
		smb_stack(&smb, SMB_STACK_POP);
	truncsp(msgbuf);
	/* ToDo: split body/tail */

	memset(&msg, 0, sizeof(msg));
	msg.hdr.attr = msgattr;
	msg.hdr.when_written = smb_when(time(NULL), sys_timezone(&cfg));
	msg.hdr.when_imported.time = time32(NULL);
	msg.hdr.when_imported.zone = msg.hdr.when_written.zone;
	msg.hdr.number = smb.status.last_msg + 1; /* this *should* be the new message number */
	smb_hfield_str(&msg, RECIPIENT, touser);
	SAFECOPY(str, cfg.sub[subnum]->misc & SUB_NAME ? useron.name : useron.alias);
	smb_hfield_str(&msg, SENDER, str);
	snprintf(str, sizeof str, "%u", useron.number);
	smb_hfield_str(&msg, SENDEREXT, str);
	/* Security logging */
	msg_client_hfields(&msg, &client);
	smb_hfield_str(&msg, SENDERSERVER, server_host_name());
	if (remsg != NULL && remsg->subj != NULL && strcmp(title, org_title) == 0)
		SAFECOPY(title, remsg->subj); // If msg subject not changed by user, use original (possibly UTF-8 encoded) subject
	normalize_msg_hfield_encoding(charset, title, sizeof title);
	smb_hfield_str(&msg, SUBJECT, title);
	add_msg_ids(&cfg, &smb, &msg, remsg);
	editor_info_to_msg(&msg, editor, charset);
		smb_hfield_str(&msg, SMB_TAGS, tags);
	i = smb_addmsg(&smb, &msg, storage, dupechk_hashes, xlat, (uchar*)msgbuf, findsig(msgbuf));
	if (i == SMB_DUPE_MSG)
		bprintf(text[CantPostMsg], "duplicate");
	else if (i != SMB_SUCCESS)
		errormsg(WHERE, ERR_WRITE, smb.file, i, smb.last_error);
	smb_stack(&smb, SMB_STACK_POP);
	smb_freemsgmem(&msg);
	if (i != SMB_SUCCESS) {
		if (i == SMB_DUPE_MSG)
			llprintf(LOG_NOTICE, "P!", "duplicate message post attempt in %s", cfg.sub[subnum]->code);
		else
			llprintf(LOG_NOTICE, "P!", "message posting failure (SMB error %d) in %s", i, cfg.sub[subnum]->code);
		return false;
	}
	bprintf(text[Posted], cfg.grp[cfg.sub[subnum]->grp]->sname
	        , cfg.sub[subnum]->lname);
	snprintf(str, sizeof str, "posted to %s on %s %s"
	         , touser, cfg.grp[cfg.sub[subnum]->grp]->sname, cfg.sub[subnum]->lname);
	logline("P+", str);
	char topic[128];
	snprintf(topic, sizeof(topic), "post/%s", cfg.sub[subnum]->code);
	snprintf(str, sizeof(str), "%u\t%s\t%u\t%u\t%s\t%s"
	         , useron.number, useron.alias, useron.ptoday, useron.posts, touser, title);
	mqtt_pub_timestamped_msg(mqtt, TOPIC_BBS_ACTION, topic, time(NULL), str);

	if (!(msgattr & MSG_ANONYMOUS)
	    && stricmp(touser, "All") != 0
	    && (remsg == NULL || remsg->from_net.type == NET_NONE)) {
		if (cfg.sub[subnum]->misc & SUB_NAME)
			i = finduserstr(0, USER_NAME, touser);
		else
			i = matchuser(&cfg, touser, TRUE /* sysop_alias */);
		if (i > 0 && i != useron.number) {
			SAFEPRINTF4(str, text[MsgPostedToYouVia]
			            , cfg.sub[subnum]->misc & SUB_NAME ? useron.name : useron.alias
			            , connection
			            , cfg.grp[cfg.sub[subnum]->grp]->sname, cfg.sub[subnum]->lname);
	signal_sub_sem(&cfg, subnum);
// When message body is UTF-8 encoded, insure header files are UTF-8 (not CP437) encoded too
extern "C" void normalize_msg_hfield_encoding(const char* charset, char* str, size_t size)
{
	char utf8_str[128];

	if (charset != NULL && strcmp(charset, FIDO_CHARSET_UTF8) == 0) {
		if (!str_is_ascii(str) && !utf8_str_is_valid(str))
			if (cp437_to_utf8_str(str, utf8_str, sizeof utf8_str, '\x80') > 1)
extern "C" void signal_sub_sem(scfg_t* cfg, int subnum)
	char str[MAX_PATH + 1];
	if (!subnum_is_valid(cfg, subnum))
	if (cfg->sub[subnum]->misc & SUB_FIDO && cfg->echomail_sem[0])
		ftouch(cmdstr(cfg, NULL, cfg->echomail_sem, nulstr, nulstr, str, sizeof(str)));
	if (cfg->sub[subnum]->post_sem[0])
		ftouch(cmdstr(cfg, NULL, cfg->sub[subnum]->post_sem, nulstr, nulstr, str, sizeof(str)));
extern "C" int msg_client_hfields(smbmsg_t* msg, client_t* client)
	int  i;
	char port[16];
	char date[64];
	if (client == NULL)
	if (client->usernum && (i = smb_hfield_str(msg, SENDERUSERID, client->user)) != SMB_SUCCESS)
	if (client->time
	    && (i = smb_hfield_str(msg, SENDERTIME, xpDateTime_to_isoDateTimeStr(gmtime_to_xpDateTime(client->time)
	                                                                         , /* separators: */ "", "", "", /* precision: */ 0
	                                                                         , date, sizeof(date)))) != SMB_SUCCESS)
	if (*client->addr
	    && (i = smb_hfield_str(msg, SENDERIPADDR, client->addr)) != SMB_SUCCESS)
	if (*client->host
	    && (i = smb_hfield_str(msg, SENDERHOSTNAME, client->host)) != SMB_SUCCESS)
	if ((i = smb_hfield_str(msg, SENDERPROTOCOL, client->protocol)) != SMB_SUCCESS)
	if (client->port) {
		SAFEPRINTF(port, "%u", client->port);
		return smb_hfield_str(msg, SENDERPORT, port);
/* Note: finds signature delimiter automatically and (if applicable) separates msgbuf into body and tail */
/* Adds/generates Message-IDs when needed */
/* Auto-sets the UTF-8 indicators for UTF-8 encoded header fields and body text */
/* If you want to save a message body with CP437 chars that also happen to be valid UTF-8 sequences, you'll need to preset the ftn_charset header */
extern "C" int savemsg(scfg_t* cfg, smb_t* smb, smbmsg_t* msg, client_t* client, const char* server, char* msgbuf, smbmsg_t* remsg)
	ushort xlat = XLAT_NONE;
	int    i;
	int    dupechk_hashes = SMB_HASH_SOURCE_DUPE;
	if (!SMB_IS_OPEN(smb)) {
		if (smb->subnum == INVALID_SUB)
			SAFEPRINTF(smb->file, "%smail", cfg->data_dir);
			SAFEPRINTF2(smb->file, "%s%s", cfg->sub[smb->subnum]->data_dir, cfg->sub[smb->subnum]->code);
		smb->retry_time = cfg->smb_retry_time;
		if ((i = smb_open(smb)) != SMB_SUCCESS)
	/* Lock the msgbase early to preserve our message number (used in MSG-IDs) */
	if (!smb->locked && smb_locksmbhdr(smb) != SMB_SUCCESS)
		return SMB_ERR_LOCK;
	if (filelength(fileno(smb->shd_fp)) > 0 && (i = smb_getstatus(smb)) != SMB_SUCCESS) {
		if (smb->locked)
	if (smb->subnum == INVALID_SUB) {  /* e-mail */
		smb->status.max_crcs = cfg->mail_maxcrcs;
		smb->status.max_age = cfg->mail_maxage;
		smb->status.max_msgs = 0; /* unlimited */
		smb->status.attr = SMB_EMAIL;
		/* duplicate message-IDs must be allowed in mail database */
		dupechk_hashes &= ~(1 << SMB_HASH_SOURCE_MSG_ID);
	} else if (subnum_is_valid(cfg, smb->subnum)) {  /* sub-board */
		smb->status.max_crcs = cfg->sub[smb->subnum]->maxcrcs;
		smb->status.max_msgs = cfg->sub[smb->subnum]->maxmsgs;
		smb->status.max_age = cfg->sub[smb->subnum]->maxage;
		smb->status.attr = 0;
		if (cfg->sub[smb->subnum]->misc & SUB_LZH)
			xlat = XLAT_LZH;
		/* enforce anonymous/private posts only */
		if (cfg->sub[smb->subnum]->misc & SUB_PONLY)
			msg->hdr.attr |= MSG_PRIVATE;
		if (cfg->sub[smb->subnum]->misc & SUB_AONLY)
			msg->hdr.attr |= MSG_ANONYMOUS;
	if (msg->hdr.when_imported.time == 0) {
		msg->hdr.when_imported.time = time32(NULL);
		msg->hdr.when_imported.zone = sys_timezone(cfg);
	if (msg->hdr.when_written.time == 0)   /* Uninitialized */
		msg->hdr.when_written = smb_when(msg->hdr.when_imported.time, msg->hdr.when_imported.zone);
	msg->hdr.number = smb->status.last_msg + 1;     /* needed for MSG-ID generation */
	if (smb->status.max_crcs == 0) /* no CRC checking means no body text dupe checking */
		dupechk_hashes &= ~(1 << SMB_HASH_SOURCE_BODY);

	if (client != NULL)
		msg_client_hfields(msg, client);
	if (server != NULL)
		smb_hfield_str(msg, SENDERSERVER, server);
	add_msg_ids(cfg, smb, msg, remsg);
	if ((msg->to != NULL && !str_is_ascii(msg->to) && utf8_str_is_valid(msg->to))
	    || (msg->from != NULL && !str_is_ascii(msg->from) && utf8_str_is_valid(msg->from))
	    || (msg->subj != NULL && !str_is_ascii(msg->subj) && utf8_str_is_valid(msg->subj)))
		msg->hdr.auxattr |= MSG_HFIELDS_UTF8;

	if (msg->ftn_charset == NULL && !str_is_ascii(msgbuf) && utf8_str_is_valid(msgbuf))
		smb_hfield_str(msg, FIDOCHARSET, FIDO_CHARSET_UTF8);

	if (msgbuf == NULL)
	if ((i = smb_addmsg(smb, msg, smb_storage_mode(cfg, smb), dupechk_hashes, xlat, (uchar*)msgbuf, findsig(msgbuf))) == SMB_SUCCESS
	    && msg->to != NULL /* no recipient means no header created at this stage */) {
		if (smb->subnum == INVALID_SUB) {
			if (msg->to_net.type == NET_FIDO && cfg->netmail_sem[0]) {
				ftouch(cmdstr(cfg, NULL, cfg->netmail_sem, nulstr, nulstr, tmp, sizeof(tmp)));
rswindell's avatar
rswindell committed
		} else
			signal_sub_sem(cfg, smb->subnum);
		if (msg->to_net.type == NET_NONE && !(msg->hdr.attr & MSG_ANONYMOUS) && cfg->text != NULL) {
			if (msg->to_ext != NULL)
			else if (subnum_is_valid(cfg, smb->subnum) && (cfg->sub[smb->subnum]->misc & SUB_NAME))
				usernum = finduserstr(cfg, 0, USER_NAME, msg->to, /* del: */ FALSE, /* next: */ FALSE, NULL, NULL);
				usernum = matchuser(cfg, msg->to, TRUE /* sysop_alias */);
			if (usernum > 0 && (client == NULL || usernum != (int)client->usernum)) {
				if (smb->subnum == INVALID_SUB) {
					safe_snprintf(str, sizeof(str), cfg->text[UserSentYouMail], msg->from);
					const char* via = smb_netaddrstr(&msg->from_net, fido_buf);
						via = (client == NULL) ? "" : client->protocol;
					safe_snprintf(str, sizeof(str), cfg->text[MsgPostedToYouVia]
					              , msg->from
					              , via
					              , cfg->grp[cfg->sub[smb->subnum]->grp]->sname, cfg->sub[smb->subnum]->lname);
rswindell's avatar
rswindell committed
	}
extern "C" int votemsg(scfg_t* cfg, smb_t* smb, smbmsg_t* msg, const char* smsgfmt, const char* votefmt)
	if (msg->hdr.when_imported.time == 0) {
		msg->hdr.when_imported.time = time32(NULL);
		msg->hdr.when_imported.zone = sys_timezone(cfg);
	}

	add_msg_ids(cfg, smb, msg, /* remsg: */ NULL);
	/* Look-up thread_back if RFC822 Reply-ID was specified */
	if (msg->hdr.thread_back == 0 && msg->reply_id != NULL) {
		if (smb_getmsgidx_by_msgid(smb, &remsg, msg->reply_id) == SMB_SUCCESS)
			msg->hdr.thread_back = remsg.idx.number;    /* poll or message being voted on */
	if (msg->hdr.thread_back == 0) {
		safe_snprintf(smb->last_error, sizeof(smb->last_error), "%s thread_back field is zero (reply_id=%s, ftn_reply=%s)"
		              , __FUNCTION__, msg->reply_id, msg->ftn_reply);
		return SMB_ERR_HDR_FIELD;
	}
	if (smb_voted_already(smb, msg->hdr.thread_back, msg->from, (enum smb_net_type)msg->from_net.type, msg->from_net.addr))
	remsg.hdr.number = msg->hdr.thread_back;
	if ((result = smb_getmsgidx(smb, &remsg)) != SMB_SUCCESS)
		return result;
	if ((result = smb_getmsghdr(smb, &remsg)) != SMB_SUCCESS)
		return result;
	if (remsg.hdr.auxattr & POLL_CLOSED)
		result = SMB_CLOSED;
	else
		result = smb_addvote(smb, msg, smb_storage_mode(cfg, smb));
	if (result == SMB_SUCCESS && smsgfmt != NULL && remsg.from_ext != NULL) {
		user_t user;
		ZERO_VAR(user);
		user.number = atoi(remsg.from_ext);
		if (getuserdat(cfg, &user) == 0 &&
		    (stricmp(remsg.from, user.alias) == 0 || stricmp(remsg.from, user.name) == 0)) {
			char from[256];
			char tstr[128];
			char smsg[4000];
			char votes[3000] = "";
			if (msg->from_net.type)
				safe_snprintf(from, sizeof(from), "%s (%s)", msg->from, smb_netaddr(&msg->from_net));
			else
				SAFECOPY(from, msg->from);
			if (remsg.hdr.type == SMB_MSG_TYPE_POLL && votefmt != NULL) {
				for (int i = 0; i < remsg.total_hfields; i++) {
					if (remsg.hfield[i].type == SMB_POLL_ANSWER) {
						if (msg->hdr.votes & (1 << answers)) {
							SAFEPRINTF(vote, votefmt, (char*)remsg.hfield_dat[i]);
			safe_snprintf(smsg, sizeof(smsg), smsgfmt
			              , timestr(cfg, (time32_t)smb_time(msg->hdr.when_written), tstr)
			              , cfg->grp[cfg->sub[smb->subnum]->grp]->sname
			              , cfg->sub[smb->subnum]->sname
			              , from
			              , remsg.subj);
			putsmsg(cfg, user.number, smsg);
	smb_freemsgmem(&remsg);
	return result;
}

extern "C" int closepoll(scfg_t* cfg, smb_t* smb, uint32_t msgnum, const char* username)
	smbmsg_t msg;

	ZERO_VAR(msg);

	msg.hdr.when_imported.time = time32(NULL);
	msg.hdr.when_imported.zone = sys_timezone(cfg);
	msg.hdr.when_written = smb_when(time(NULL), msg.hdr.when_imported.zone);
	msg.hdr.thread_back = msgnum;
	smb_hfield_str(&msg, SENDER, username);
	add_msg_ids(cfg, smb, &msg, /* remsg: */ NULL);
	result = smb_addpollclosure(smb, &msg, smb_storage_mode(cfg, smb));

	smb_freemsgmem(&msg);
extern "C" int postpoll(scfg_t* cfg, smb_t* smb, smbmsg_t* msg)
	if (msg->hdr.when_imported.time == 0) {
		msg->hdr.when_imported.time = time32(NULL);
		msg->hdr.when_imported.zone = sys_timezone(cfg);
	}
	if (msg->hdr.when_written.time == 0)
		msg->hdr.when_written = smb_when(msg->hdr.when_imported.time, msg->hdr.when_imported.zone);
	add_msg_ids(cfg, smb, msg, /* remsg: */ NULL);
	return smb_addpoll(smb, msg, smb_storage_mode(cfg, smb));
}

// Send an email and a short-message to a local user about something important (e.g. a system error)
extern "C" int notify(scfg_t* cfg, uint usernumber, const char* subject, const char* text)
	int      i;
	smb_t    smb = {};
	uint16_t xlat;
	int      storage;
	int      dupechk_hashes;
	uint16_t agent = AGENT_PROCESS;
	uint16_t nettype = NET_UNKNOWN;
	smbmsg_t msg = {};
	user_t   user = {};
	char     str[128];
	if ((i = getuserdat(cfg, &user)) != 0)
		return i;

	msg.hdr.when_imported.time = time32(NULL);
	msg.hdr.when_imported.zone = sys_timezone(cfg);
	msg.hdr.when_written = smb_when(time(NULL), msg.hdr.when_imported.zone);
	smb_hfield(&msg, SENDERAGENT, sizeof(agent), &agent);
	smb_hfield_str(&msg, SENDER, cfg->sys_name);
	smb_hfield_str(&msg, RECIPIENT, user.alias);
	if (cfg->sys_misc & SM_FWDTONET && user.misc & NETMAIL && user.netmail[0]) {
		smb_hfield_netaddr(&msg, RECIPIENTNETADDR, user.netmail, &nettype);
		smb_hfield_bin(&msg, RECIPIENTNETTYPE, nettype);
	} else {
		SAFEPRINTF(str, "%u", usernumber);
		smb_hfield_str(&msg, RECIPIENTEXT, str);
	}
	char* msgsubj = strip_ctrl(subject, NULL);
	smb_hfield_str(&msg, SUBJECT, msgsubj);
	free(msgsubj);
	if (msgbase_open(cfg, &smb, INVALID_SUB, &storage, &dupechk_hashes, &xlat) == SMB_SUCCESS) {
		add_msg_ids(cfg, &smb, &msg, /* remsg: */ NULL);
		smb_addmsg(&smb, &msg, storage, dupechk_hashes, xlat, (uchar*)text, /* tail: */ NULL);
		smb_close(&smb);
	}
	smb_freemsgmem(&msg);

	char smsg[1024];
	if (text != NULL)
		safe_snprintf(smsg, sizeof(smsg), "\1n\1h%s \1r%s:\r\n%s\1n\r\n"
		              , timestr(cfg, msg.hdr.when_imported.time, str)
		              , subject
		              , text);
		safe_snprintf(smsg, sizeof(smsg), "\1n\1h%s \1r%s\1n\r\n"
		              , timestr(cfg, msg.hdr.when_imported.time, str)
		              , subject);
	return putsmsg(cfg, usernumber, smsg);
}

bool sbbs_t::notify(const char* subject, const char* text)
{
	char buf[128];

	return ::notify(&cfg, /* usernumber: */ 1, expand_atcodes(subject, buf, sizeof buf), text) == 0;