Newer
Older
/* Synchronet message retrieval functions */
/* $Id$ */
// vi: tabstop=4
/****************************************************************************
* @format.tab-size 4 (Plain Text/Source Code File Header) *
* @format.use-tabs true (see http://www.synchro.net/ptsc_hdr.html) *
* *
* 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 *
* *
* Anonymous FTP access to the most recent released source is available at *
* ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net *
* *
* Anonymous CVS access to the development source and modification history *
* is available at cvs.synchro.net:/cvsroot/sbbs, example: *
* cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login *
* (just hit return, no password is necessary) *
* cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src *
* *
* For Synchronet coding style and modification guidelines, see *
* http://www.synchro.net/source.html *
* *
* You are encouraged to submit any modifications (preferably in Unix diff *
* format) via e-mail to mods@synchro.net *
* *
* Note: If this box doesn't appear square, then you need to fix your tabs. *
****************************************************************************/
/***********************************************************************/
/* Functions that do i/o with messages (posts/mail/auto) or their data */
/***********************************************************************/
#include "sbbs.h"
#include "utf8.h"
/****************************************************************************/
/* Loads an SMB message from the open msg base the fastest way possible */
/* first by offset, and if that's the wrong message, then by number. */
/* Returns >=0 if the message was loaded and left locked, otherwise < 0. */
/* !WARNING!: If you're going to write the msg index back to disk, you must */
/* Call this function with a msg->idx.offset of 0 (so msg->offset will be */
/* initialized correctly) */
/****************************************************************************/
int sbbs_t::loadmsg(smbmsg_t *msg, ulong number)
{
char str[128];
int i;
if(msg->idx.offset) { /* Load by offset if specified */
if((i=smb_lockmsghdr(&smb,msg)) != SMB_SUCCESS) {
errormsg(WHERE,ERR_LOCK,smb.file,i,smb.last_error);
return i;
if(i==SMB_SUCCESS) {
if(msg->hdr.number==number)
return msg->total_hfields;
/* Wrong offset */
smb_unlockmsghdr(&smb,msg);
if((i=smb_getmsgidx(&smb,msg))!=SMB_SUCCESS) /* Message is deleted */
return i;
if((i=smb_lockmsghdr(&smb,msg))!=SMB_SUCCESS) {
errormsg(WHERE,ERR_LOCK,smb.file,i,smb.last_error);
return i;
if((i=smb_getmsghdr(&smb,msg))!=SMB_SUCCESS) {
SAFEPRINTF4(str,"(%06" PRIX32 ") #%" PRIu32 "/%lu %s",msg->idx.offset,msg->idx.number
,number,smb.file);
smb_unlockmsghdr(&smb,msg);
errormsg(WHERE,ERR_READ,str,i,smb.last_error);
return i;
return msg->total_hfields;
uint16_t attr = msg->hdr.attr;
uint16_t poll = attr&MSG_POLL_VOTE_MASK;
uint32_t auxattr = msg->hdr.auxattr;
uint32_t netattr = msg->hdr.netattr;
bprintf(text[MsgAttr]
,attr&MSG_PRIVATE ? "Private " :nulstr
,attr&MSG_SPAM ? "SPAM " :nulstr
,attr&MSG_READ ? "Read " :nulstr
,attr&MSG_DELETE ? "Deleted " :nulstr
,attr&MSG_KILLREAD ? "Kill " :nulstr
,attr&MSG_ANONYMOUS ? "Anonymous " :nulstr
,attr&MSG_LOCKED ? "Locked " :nulstr
,attr&MSG_PERMANENT ? "Permanent " :nulstr
,attr&MSG_MODERATED ? "Moderated " :nulstr
,attr&MSG_VALIDATED ? "Validated " :nulstr
,attr&MSG_REPLIED ? "Replied " :nulstr
,attr&MSG_NOREPLY ? "NoReply " :nulstr
,poll == MSG_POLL ? "Poll " :nulstr
,poll == MSG_POLL && auxattr&POLL_CLOSED ? "(Closed) " :nulstr
,auxattr&(MSG_FILEATTACH|MSG_MIMEATTACH) ? "Attach " :nulstr
,netattr&MSG_SENT ? "Sent " :nulstr
,netattr&MSG_INTRANSIT ? "InTransit ":nulstr
/* Returns a CP437 text.dat string converted to UTF-8, when appropriate */
const char* sbbs_t::msghdr_text(const smbmsg_t* msg, uint index)
{
if(msg == NULL || !(msg->hdr.auxattr & MSG_HFIELDS_UTF8))
return text[index];
if(cp437_to_utf8_str(text[index], msghdr_utf8_text, sizeof(msghdr_utf8_text), /* min-char-val: */'\x80') < 1)
return text[index];
return msghdr_utf8_text;
}
// Returns a CP437 version of a message header field or UTF-8 if can_utf8 is true
// Doesn't do CP437->UTF-8 conversion
const char* sbbs_t::msghdr_field(const smbmsg_t* msg, const char* str, char* buf, bool can_utf8)
{
if(msg == NULL || !(msg->hdr.auxattr & MSG_HFIELDS_UTF8))
return str;
if(can_utf8 && term_supports(UTF8))
return str;
if(buf == NULL)
buf = msgghdr_field_cp437_str;
strncpy(buf, str, sizeof(msgghdr_field_cp437_str));
utf8_normalize_str(buf);
utf8_replace_chars(buf, unicode_to_cp437
,/* unsupported char: */CP437_INVERTED_QUESTION_MARK
,/* unsupported zero-width ch: */0
,/* decode error char: */CP437_INVERTED_EXCLAMATION_MARK);
return buf;
}
/****************************************************************************/
/* Displays a message header to the screen */
/****************************************************************************/
void sbbs_t::show_msghdr(smb_t* smb, smbmsg_t* msg, const char* subject, const char* from, const char* to)
char str[MAX_PATH+1];
char age[64];
long pmode = 0;
if(smb != NULL)
this->smb = *smb; // Needed for @-codes and JS bbs.smb_* properties
current_msg = msg; // Needed for @-codes and JS bbs.msg_* properties
current_msg_subj = msg->subj;
current_msg_from = msg->from;
current_msg_to = msg->to;
if(msg->hdr.auxattr & MSG_HFIELDS_UTF8)
pmode |= P_UTF8;
}
if(subject != NULL)
current_msg_subj = subject;
if(from != NULL)
current_msg_from = from;
if(to != NULL)
current_msg_to = to;
attr(LIGHTGRAY);
if(!tos) {
if(useron.misc&CLRSCRN)
outchar(FF);
else
CRLF;
}
if(!menu("msghdr", P_NOERROR)) {
bprintf(pmode, msghdr_text(msg, MsgSubj), current_msg_subj);
if(msg->tags && *msg->tags)
bprintf(text[MsgTags], msg->tags);
if(msg->hdr.attr || msg->hdr.netattr || (msg->hdr.auxattr & ~MSG_HFIELDS_UTF8))
show_msgattr(msg);
bprintf(pmode, msghdr_text(msg, MsgTo), current_msg_to);
if(msg->to_net.addr!=NULL)
bprintf(text[MsgToNet],smb_netaddrstr(&msg->to_net,str));
if(msg->to_ext)
bprintf(text[MsgToExt],msg->to_ext);
}
if(msg->cc_list != NULL)
bprintf(text[MsgCarbonCopyList], msg->cc_list);
if(current_msg_from != NULL && (!(msg->hdr.attr&MSG_ANONYMOUS) || SYSOP)) {
bprintf(pmode, msghdr_text(msg, MsgFrom), current_msg_from);
if(msg->from_ext)
bprintf(text[MsgFromExt],msg->from_ext);
if(msg->from_net.addr!=NULL && (current_msg_from == NULL || strchr(current_msg_from,'@')==NULL))
bprintf(text[MsgFromNet],smb_netaddrstr(&msg->from_net,str));
}
if(!(msg->hdr.attr&MSG_POLL) && (msg->upvotes || msg->downvotes))
bprintf(text[MsgVotes]
,msg->upvotes, msg->user_voted==1 ? text[PollAnswerChecked] : nulstr
,msg->downvotes, msg->user_voted==2 ? text[PollAnswerChecked] : nulstr
,msg->upvotes - msg->downvotes);
bprintf(text[MsgDate]
,timestr(msg->hdr.when_written.time)
,smb_zonestr(msg->hdr.when_written.zone,NULL)
,age_of_posted_item(age, sizeof(age), msg->hdr.when_written.time - (smb_tzutc(msg->hdr.when_written.zone) * 60)));
}
for(i=0;i<msg->total_hfields;i++) {
if(msg->hfield[i].type==SENDER)
sender=(char *)msg->hfield_dat[i];
if(msg->hfield[i].type==FORWARDED && sender)
bprintf(text[ForwardedFrom],sender
,timestr(*(time32_t *)msg->hfield_dat[i]));
current_msg_subj = NULL;
current_msg_from = NULL;
current_msg_to = NULL;
}
/****************************************************************************/
/* Displays message header and text (if not deleted) */
/****************************************************************************/
bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, long p_mode, post_t* post)
if((msg->hdr.type == SMB_MSG_TYPE_NORMAL && post != NULL && (post->upvotes || post->downvotes))
|| msg->hdr.type == SMB_MSG_TYPE_POLL)
msg->user_voted = smb_voted_already(smb, msg->hdr.number
,cfg.sub[smb->subnum]->misc&SUB_NAME ? useron.name : useron.alias, NET_NONE, NULL);
if(msg->hdr.type == SMB_MSG_TYPE_POLL && post != NULL && smb->subnum < cfg.total_subs) {
int comments=0;
for(int i = 0; i < msg->total_hfields; i++)
if(msg->hfield[i].type == SMB_COMMENT) {
bprintf("%s\r\n", (char*)msg->hfield_dat[i]);
comments++;
}
if(comments)
CRLF;
for(int i = 0; i < msg->total_hfields; i++) {
if(msg->hfield[i].type != SMB_POLL_ANSWER)
continue;
answer = (char*)msg->hfield_dat[i];
int len = strlen(answer);
if(len > longest_answer)
longest_answer = len;
}
unsigned answers = 0;
for(int i = 0; i < msg->total_hfields; i++) {
if(msg->hfield[i].type != SMB_POLL_ANSWER)
continue;
answer = (char*)msg->hfield_dat[i];
float pct = post->total_votes ? ((float)post->votes[answers] / post->total_votes)*100.0F : 0.0F;
char str[128];
int width = longest_answer;
if(width < cols/3) width = cols/3;
else if(width > cols-20)
width = cols-20;
bprintf(text[PollAnswerNumber], answers+1);
bool results_visible = false;
if((msg->hdr.auxattr&POLL_RESULTS_MASK) == POLL_RESULTS_OPEN)
results_visible = true;
else if((msg->from_net.type == NET_NONE && sub_op(smb->subnum))
|| smb_msg_is_from(msg, cfg.sub[smb->subnum]->misc&SUB_NAME ? useron.name : useron.alias, NET_NONE, NULL))
results_visible = true;
else if((msg->hdr.auxattr&POLL_RESULTS_MASK) == POLL_RESULTS_CLOSED)
results_visible = (msg->hdr.auxattr&POLL_CLOSED) ? true : false;
else if((msg->hdr.auxattr&POLL_RESULTS_MASK) != POLL_RESULTS_SECRET)
results_visible = msg->user_voted ? true : false;
safe_snprintf(str, sizeof(str), text[PollAnswerFmt]
,width, width, answer, post->votes[answers], pct);
backfill(str, pct, cfg.color[clr_votes_full], cfg.color[clr_votes_empty]);
if(msg->user_voted&(1<<answers))
bputs(text[PollAnswerChecked]);
} else {
attr(cfg.color[clr_votes_empty]);
bputs(answer);
}
if(!msg->user_voted && !(useron.misc&EXPERT) && !(msg->hdr.auxattr&POLL_CLOSED) && !(useron.rest&FLAG('V')))
if((txt=smb_getmsgtxt(smb, msg, 0)) == NULL)
return false;
char* p = txt;
if(!(console&CON_RAW_IN)) {
if(strstr(txt, "\x1b[") == NULL) // Don't word-wrap raw ANSI text
p_mode|=P_WORDWRAP;
p = smb_getplaintext(msg, txt);
if(p == NULL)
p = txt;
else
bprintf(text[MIMEDecodedPlainTextFmt]
, msg->text_charset == NULL ? "unspecified (US-ASCII)" : msg->text_charset
, msg->text_subtype);
if(smb_msg_is_utf8(msg)) {
if(!term_supports(UTF8))
utf8_normalize_str(txt);
p_mode |= P_UTF8;
}
if(smb->subnum < cfg.total_subs) {
p_mode |= cfg.sub[smb->subnum]->pmode;
p_mode &= ~cfg.sub[smb->subnum]->n_pmode;
}
putmsg(p, p_mode, msg->columns);
smb_freemsgtxt(txt);
if(column)
CRLF;
if((txt=smb_getmsgtxt(smb,msg,GETMSGTXT_TAIL_ONLY))==NULL)
return false;
putmsg(txt, p_mode&(~P_WORDWRAP));
smb_freemsgtxt(txt);
return true;
void sbbs_t::download_msg_attachments(smb_t* smb, smbmsg_t* msg, bool del)
{
char str[256];
char fpath[MAX_PATH+1];
char* txt;
int attachment_index = 0;
bool found = true;
while((txt=smb_getmsgtxt(smb, msg, 0)) != NULL && found) {
char filename[MAX_PATH+1] = {0};
uint32_t filelen = 0;
uint8_t* filedata;
if((filedata = smb_getattachment(msg, txt, filename, sizeof(filename), &filelen, attachment_index++)) != NULL
&& filename[0] != 0 && filelen > 0) {
char tmp[32];
SAFEPRINTF2(str, text[DownloadAttachedFileQ], filename, ultoac(filelen,tmp));
if(!noyes(str)) {
SAFEPRINTF2(fpath, "%s%s", cfg.temp_dir, filename);
FILE* fp = fopen(fpath, "wb");
if(fp == NULL)
errormsg(WHERE, ERR_OPEN, fpath, 0);
else {
int result = fwrite(filedata, filelen, 1, fp);
fclose(fp);
if(!result)
errormsg(WHERE, ERR_WRITE, fpath, filelen);
else
sendfile(fpath, useron.prot, "attachment");
}
}
} else
found = false;
smb_freemsgtxt(txt);
}
if(msg->hdr.auxattr&MSG_FILEATTACH) { /* Attached file */
char subj[FIDO_SUBJ_LEN];
smb_getmsgidx(smb, msg);
SAFECOPY(subj, msg->subj); /* filenames (multiple?) in title */
tp=subj;
while(online) {
p=strchr(tp,' ');
if(p) *p=0;
tp=getfname(tp);
file_t fd;
fd.dir=cfg.total_dirs+1; /* temp dir for file attachments */
padfname(tp,fd.name);
SAFEPRINTF3(fpath,"%sfile/%04u.in/%s" /* path is path/fname */
,cfg.data_dir, msg->idx.to, tp);
if(!fexistcase(fpath) && msg->idx.from)
SAFEPRINTF3(fpath,"%sfile/%04u.out/%s" /* path is path/fname */
,cfg.data_dir, msg->idx.from,tp);
long length=(long)flength(fpath);
if(length<1)
bprintf(text[FileDoesNotExist], tp);
else if(!(useron.exempt&FLAG('T')) && cur_cps && !SYSOP
&& length/(long)cur_cps>(time_t)timeleft)
bputs(text[NotEnoughTimeToDl]);
else {
char tmp[512];
int i;
SAFEPRINTF2(str, text[DownloadAttachedFileQ]
,getfname(fpath),ultoac(length,tmp));
if(length>0L && text[DownloadAttachedFileQ][0] && yesno(str)) {
{ /* Remote User */
xfer_prot_menu(XFER_DOWNLOAD);
mnemonics(text[ProtocolOrQuit]);
strcpy(str,"Q");
for(i=0;i<cfg.total_prots;i++)
if(cfg.prot[i]->dlcmd[0]
&& chk_ar(cfg.prot[i]->ar,&useron,&client)) {
sprintf(tmp,"%c",cfg.prot[i]->mnemonic);
strcat(str,tmp);
}
ch=(char)getkeys(str,0);
for(i=0;i<cfg.total_prots;i++)
if(cfg.prot[i]->dlcmd[0] && ch==cfg.prot[i]->mnemonic
&& chk_ar(cfg.prot[i]->ar,&useron,&client))
break;
if(i<cfg.total_prots) {
int error = protocol(cfg.prot[i], XFER_DOWNLOAD, fpath, nulstr, false);
if(checkprotresult(cfg.prot[i],error,&fd)) {
if(del)
remove(fpath);
logon_dlb+=length; /* Update stats */
logon_dls++;
useron.dls=(ushort)adjustuserrec(&cfg,useron.number
,U_DLS,5,1);
useron.dlb=adjustuserrec(&cfg,useron.number
,U_DLB,10,length);
bprintf(text[FileNBytesSent]
,fd.name,ultoac(length,tmp));
SAFEPRINTF(str
,"downloaded attached file: %s"
,fd.name);
logline("D-",str);
}
autohangup();
}
}
}
}
if(!p)
break;
tp=p+1;
while(*tp==' ') tp++;
}
// Remove the *.in directory, only if its empty
SAFEPRINTF2(fpath, "%sfile/%04u.in", cfg.data_dir, msg->idx.to);
rmdir(fpath);
}
}
/****************************************************************************/
/* Writes message header and text data to a text file */
/****************************************************************************/
bool sbbs_t::msgtotxt(smb_t* smb, smbmsg_t* msg, const char *fname, bool header, ulong gettxt_mode)
char tmp[128];
if((out=fnopen(&i,fname,O_WRONLY|O_CREAT|O_APPEND))==NULL) {
errormsg(WHERE,ERR_OPEN,fname,0);
return false;
if(header) {
fprintf(out,"\r\n");
fprintf(out,"Subj : %s\r\n",msg->subj);
fprintf(out,"To : %s",msg->to);
if(msg->to_net.addr)
fprintf(out," (%s)",smb_netaddrstr(&msg->to_net,tmp));
if(msg->to_ext)
fprintf(out," #%s",msg->to_ext);
fprintf(out,"\r\nFrom : %s",msg->from);
if(msg->from_ext && !(msg->hdr.attr&MSG_ANONYMOUS))
fprintf(out," #%s",msg->from_ext);
if(msg->from_net.addr)
fprintf(out," (%s)",smb_netaddrstr(&msg->from_net,tmp));
fprintf(out,"\r\nDate : %.24s %s"
,timestr(msg->hdr.when_written.time)
,smb_zonestr(msg->hdr.when_written.zone,NULL));
fprintf(out,"\r\n\r\n");
bool result = false;
buf=smb_getmsgtxt(smb, msg, gettxt_mode);
if(buf!=NULL) {
smb_freemsgtxt(buf);
} else if(smb_getmsgdatlen(msg)>2)
errormsg(WHERE,ERR_READ,smb->file,smb_getmsgdatlen(msg));
}
/****************************************************************************/
/* Returns message number posted at or after time */
/****************************************************************************/
ulong sbbs_t::getmsgnum(uint subnum, time_t t)
{
ZERO_VAR(smb);
SAFEPRINTF2(smb.file,"%s%s",cfg.sub[subnum]->data_dir,cfg.sub[subnum]->code);
smb.retry_time=cfg.smb_retry_time;
smb.subnum=subnum;
if((i=smb_open_index(&smb)) != SMB_SUCCESS) {
errormsg(WHERE,ERR_OPEN,smb.file,i,smb.last_error);
return 0;
int result = smb_getmsgidx_by_time(&smb, &idx, t);
if(result >= SMB_SUCCESS)
return idx.number - 1;
return ~0;
}
/****************************************************************************/
/* Returns the time of the message number pointed to by 'ptr' */
/****************************************************************************/
time_t sbbs_t::getmsgtime(uint subnum, ulong ptr)
{
int i;
smb_t smb;
smbmsg_t msg;
idxrec_t lastidx;
ZERO_VAR(smb);
SAFEPRINTF2(smb.file,"%s%s",cfg.sub[subnum]->data_dir,cfg.sub[subnum]->code);
smb.retry_time=cfg.smb_retry_time;
smb.subnum=subnum;
if((i=smb_open(&smb))!=0) {
errormsg(WHERE,ERR_OPEN,smb.file,i,smb.last_error);
return(0);
if(!filelength(fileno(smb.sid_fp))) { /* Empty base */
smb_close(&smb);
return(0);
msg.offset=0;
msg.hdr.number=0;
if(smb_getmsgidx(&smb,&msg)) { /* Get first message index */
smb_close(&smb);
return(0);
if(!ptr || msg.idx.number>=ptr) { /* ptr is before first message */
smb_close(&smb);
return(msg.idx.time); /* so return time of first msg */
}
if(smb_getlastidx(&smb,&lastidx)) { /* Get last message index */
smb_close(&smb);
return(0);
if(lastidx.number<ptr) { /* ptr is after last message */
smb_close(&smb);
return(lastidx.time); /* so return time of last msg */
}
msg.idx.time=0;
msg.hdr.number=ptr;
if(!smb_getmsgidx(&smb,&msg)) {
smb_close(&smb);
return(msg.idx.time);
if(ptr-msg.idx.number < lastidx.number-ptr) {
msg.offset=0;
msg.idx.number=0;
while(msg.idx.number<ptr) {
msg.hdr.number=0;
if(smb_getmsgidx(&smb,&msg) || msg.idx.number>=ptr)
break;
msg.offset++;
return(msg.idx.time);
ptr--;
while(ptr) {
msg.hdr.number=ptr;
if(!smb_getmsgidx(&smb,&msg))
break;
smb_close(&smb);
return(msg.idx.time);
}
/****************************************************************************/
/* Returns the total number of msgs in the sub-board and sets 'ptr' to the */
/* number of the last message in the sub (0) if no messages. */
/****************************************************************************/
ulong sbbs_t::getlastmsg(uint subnum, uint32_t *ptr, time_t *t)
idxrec_t idx;
if(ptr)
(*ptr)=0;
if(t)
(*t)=0;
if(subnum>=cfg.total_subs)
return(0);
ZERO_VAR(smb);
SAFEPRINTF2(smb.file,"%s%s",cfg.sub[subnum]->data_dir,cfg.sub[subnum]->code);
smb.retry_time=cfg.smb_retry_time;
smb.subnum=subnum;
if((i=smb_open(&smb))!=0) {
errormsg(WHERE,ERR_OPEN,smb.file,i,smb.last_error);
return(0);
if(!filelength(fileno(smb.sid_fp))) { /* Empty base */
smb_close(&smb);
return(0);
if((i=smb_locksmbhdr(&smb))!=0) {
smb_close(&smb);
errormsg(WHERE,ERR_LOCK,smb.file,i,smb.last_error);
return(0);
if((i=smb_getlastidx(&smb,&idx))!=0) {
smb_close(&smb);
errormsg(WHERE,ERR_READ,smb.file,i,smb.last_error);
return(0);
total=(long)filelength(fileno(smb.sid_fp))/sizeof(idxrec_t);