564 lines
19 KiB
C
564 lines
19 KiB
C
/* NetHack 3.7 vmsmail.c $NHDT-Date: 1685522048 2023/05/31 08:34:08 $ $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.18 $ */
|
|
/* Copyright (c) Robert Patrick Rankin, 1991. */
|
|
/* NetHack may be freely redistributed. See license for details. */
|
|
|
|
#include "config.h"
|
|
#include "mail.h"
|
|
|
|
/* lint suppression due to lack of extern.h */
|
|
unsigned long init_broadcast_trapping(void);
|
|
unsigned long enable_broadcast_trapping(void);
|
|
unsigned long disable_broadcast_trapping(void);
|
|
struct mail_info *parse_next_broadcast(void);
|
|
|
|
#ifdef VMSVSI
|
|
#include <descrip.h>
|
|
#include <lib$routines.h>
|
|
#include <starlet.h>
|
|
#endif
|
|
|
|
#ifdef MAIL
|
|
#include "wintype.h"
|
|
#include "winprocs.h"
|
|
#include <ctype.h>
|
|
#include <descrip.h>
|
|
#include <errno.h>
|
|
#ifndef __GNUC__
|
|
#include <msgdef.h>
|
|
#else
|
|
#define MSG$_TRMHANGUP 6
|
|
#define MSG$_TRMBRDCST 83
|
|
#endif /*__GNUC__*/
|
|
#include <signal.h>
|
|
/* #include <string.h> */
|
|
#define vms_ok(sts) ((sts) & 1)
|
|
|
|
static struct mail_info *parse_brdcst(char *);
|
|
static void filter_brdcst(char *);
|
|
static void flush_broadcasts(void);
|
|
static void broadcast_ast(int);
|
|
extern char *eos(char *);
|
|
extern char *strstri(const char *, const char *);
|
|
extern int strncmpi(const char *, const char *, int);
|
|
|
|
extern size_t strspn(const char *, const char *);
|
|
#ifndef __DECC
|
|
extern int sscanf(const char *, const char *, ...);
|
|
#endif
|
|
|
|
#ifdef VMSVSI
|
|
#include <smg$routines.h>
|
|
#else
|
|
extern unsigned long smg$create_pasteboard(), smg$get_broadcast_message(),
|
|
smg$set_broadcast_trapping(), smg$disable_broadcast_trapping();
|
|
#endif
|
|
|
|
extern volatile int broadcasts; /* defining declaration in mail.c */
|
|
|
|
static long pasteboard_id = 0; /* SMG's magic cookie */
|
|
|
|
/*
|
|
* Mail (et al) overview:
|
|
*
|
|
* When a broadcast is asynchronously captured, a volatile counter
|
|
* ('broadcasts') is incremented. Each player turn, ckmailstatus() polls
|
|
* the counter and calls parse_next_broadcast() if it's positive; this
|
|
* returns some display text, object name, and response command, which is
|
|
* passed to newmail(). Routine newmail() generates a mail-daemon monster
|
|
* who approaches the character, "speaks" the display text, and delivers
|
|
* a scroll of mail pre-named to the object name; the response command is
|
|
* also attached to the scroll's oextra->omailcmd field.
|
|
* Unrecognized broadcasts result in the mail-daemon
|
|
* arriving and announcing the display text, but no scroll being created.
|
|
* If SHELL is undefined, then all broadcasts are treated as 'other'; since
|
|
* no subprocesses are allowed, there'd be no way to respond to the scroll.
|
|
*
|
|
* When a scroll of mail is read by the character, readmail() extracts
|
|
* the command string and uses it for the default when prompting the
|
|
* player for a system command to spawn. The player may enter any command
|
|
* he or she chooses, or just <return> to accept the default or <escape> to
|
|
* avoid executing any command. If the command is "SPAWN", a regular shell
|
|
* escape to DCL is performed; otherwise, the indicated single command is
|
|
* spawned. Either way, NetHack resumes play when the subprocess terminates
|
|
* or explicitly reattaches to its parent.
|
|
*
|
|
* Broadcast parsing:
|
|
*
|
|
* The following broadcast messages are [attempted to be] recognized:
|
|
* text fragment name for scroll default command
|
|
* New mail VMSmail MAIL
|
|
* New ALL-IN-1 MAIL A1mail A1M
|
|
* Software Tools mail STmail MSG [+folder]
|
|
* MM mail MMmail MM
|
|
* WPmail: New mail WPmail OFFICE/MAIL
|
|
* **M400 mail M400mail M400
|
|
* " mail", ^"mail " unknown mail SPAWN
|
|
* " phoning" Phone call PHONE ANSWER
|
|
* talk-daemon...by...foo Talk request TALK[/OLD] foo@bar
|
|
* (node)user - Bitnet noise XYZZY user@node
|
|
* Anything else results in just the message text being passed along, no
|
|
* scroll of mail so consequently no command to execute when scroll read.
|
|
* The user can set up ``$ XYZZY :== SEND'' prior to invoking NetHack if
|
|
* vanilla JNET responses to Bitnet messages are preferred.
|
|
*
|
|
* Static return buffers are used because only one broadcast gets
|
|
* processed at a time, and the essential information in each one is
|
|
* either displayed and discarded or copied into a scroll-of-mail object.
|
|
*
|
|
* The test driver code below can be used to check out potential new
|
|
* entries without rebuilding NetHack itself. CC/DEFINE="TEST_DRIVER"
|
|
* Link it with hacklib.obj or nethack.olb/incl=hacklib (not nethack/lib).
|
|
*/
|
|
|
|
static struct mail_info msg; /* parse_*()'s return buffer */
|
|
static char nam_buf[63], /* maximum onamelth, size of ONAME(object) */
|
|
cmd_buf[99], /* arbitrary */
|
|
txt_buf[255 + 1]; /* same size as used for message buf[] */
|
|
|
|
/* try to decipher and categorize broadcast message text
|
|
*/
|
|
static struct mail_info *
|
|
parse_brdcst(char *buf) /* called by parse_next_broadcast() */
|
|
/* input: filtered broadcast text */
|
|
{
|
|
int typ;
|
|
char *txt;
|
|
const char *nam, *cmd;
|
|
#ifdef SHELL /* only parse if spawned commands are enabled */
|
|
char *p, *q;
|
|
boolean is_jnet_send;
|
|
char user[127 + 1], node[127 + 1], sentinel;
|
|
|
|
/* Check these first; otherwise, their arbitrary text would enable
|
|
easy spoofing of some other message patterns. Unfortunately,
|
|
any home-grown broadcast delivery program poses a similar risk. */
|
|
if (!strncmpi(buf, "reply received", 14))
|
|
goto other;
|
|
is_jnet_send = (sscanf(buf, "(%[^)])%s -%c", node, user, &sentinel) == 3);
|
|
if (is_jnet_send)
|
|
goto jnet_send;
|
|
|
|
/* scan the text more or less by brute force */
|
|
if ((q = strstri(buf, " mail")) != 0 || /* all known mail broadcasts */
|
|
!strncmpi(q = buf, "mail ", 5)) { /* unexpected alternative */
|
|
typ = MSG_MAIL;
|
|
p = strstri(q, " from");
|
|
txt = p ? strcat(strcpy(txt_buf, "Mail for you"), p) : (char *) 0;
|
|
|
|
if (!strncmpi(buf, "new mail", 8)) {
|
|
/*
|
|
* New mail [on node FOO] from [SPAM::]BAR
|
|
* [\"personal_name\"] [\(HH:MM:SS\)]
|
|
*/
|
|
nam = "VMSmail"; /* assume VMSmail */
|
|
cmd = "MAIL";
|
|
if (txt && (p = strrchr(txt, '(')) > txt && /* discard time */
|
|
(--p, strspn(p, "0123456789( :.)") == strlen(p)))
|
|
*p = '\0';
|
|
} else if (!strncmpi(buf, "new all-in-1", 12)) {
|
|
int i;
|
|
/*
|
|
* New ALL-IN-1 MAIL message [on node FOO] from Personal Name
|
|
* \(BAR@SPAM\) [\(DD-MMM-YYYY HH:MM:SS\)]
|
|
*/
|
|
nam = "A1mail";
|
|
cmd = "A1M";
|
|
if (txt && (p = strrchr(txt, '(')) > txt
|
|
&& /* discard date+time */
|
|
sscanf(p - 1, " (%*d-%*[^-]-%*d %*d:%*d:%d) %c", &i,
|
|
&sentinel) == 1)
|
|
*--p = '\0';
|
|
} else if (!strncmpi(buf, "software tools", 14)) {
|
|
/*
|
|
* Software Tools mail has arrived on FOO from \'BAR\' [in SPAM]
|
|
*/
|
|
nam = "STmail";
|
|
cmd = "MSG";
|
|
if (txt && (p = strstri(p, " in ")) != 0) /* specific folder */
|
|
cmd = strcat(strcpy(cmd_buf, "MSG +"), p + 4);
|
|
} else if (q - 2 >= buf && !strncmpi(q - 2, "mm", 2)) {
|
|
/*
|
|
* {MultiNet\ |PMDF\/}MM mail has arrived on FOO from BAR\n
|
|
* [Subject: subject_text] (PMDF only)
|
|
*/
|
|
nam = "MMmail"; /* MultiNet's version of MM */
|
|
cmd = "MM"; /*{ perhaps "MM READ"? }*/
|
|
} else if (!strncmpi(buf, "wpmail:", 7)) {
|
|
/*
|
|
* WPmail: New mail from BAR. subject_text
|
|
*/
|
|
nam = "WPmail"; /* WordPerfect [sic] Office */
|
|
cmd = "OFFICE/MAIL";
|
|
} else if (!strncmpi(buf, "**m400 mail", 7)) {
|
|
/*
|
|
* **M400 mail waiting**
|
|
*/
|
|
nam = "M400mail"; /* Messenger 400 [not seen] */
|
|
cmd = "M400";
|
|
} else {
|
|
/* not recognized, but presumed to be mail */
|
|
nam = "unknown mail";
|
|
cmd = "SPAWN"; /* generic escape back to DCL */
|
|
txt = (char *) 0; /* don't rely on "from" info here */
|
|
}
|
|
|
|
if (!txt)
|
|
txt = strcat(strcpy(txt_buf, "Mail for you: "), buf);
|
|
|
|
/*
|
|
* end of mail recognition; now check for call-type interruptions...
|
|
*/
|
|
} else if ((q = strstri(buf, " phoning")) != 0) {
|
|
/*
|
|
* BAR is phoning you [on FOO] \(HH:MM:SS\)
|
|
*/
|
|
typ = MSG_CALL;
|
|
nam = "Phone call";
|
|
cmd = "PHONE ANSWER";
|
|
if (!strncmpi(q + 8, " you", 4))
|
|
q += (8 + 4), *q = '\0';
|
|
txt = strcat(strcpy(txt_buf, "Do you hear ringing? "), buf);
|
|
} else if ((q = strstri(buf, " talk-daemon")) != 0
|
|
|| (q = strstri(buf, " talk_daemon")) != 0) {
|
|
/*
|
|
* Message from TALK-DAEMON@FOO at HH:MM:SS\n
|
|
* Connection request by BAR@SPAM\n
|
|
* \[Respond with: TALK[/OLD] BAR@SPAM\]
|
|
*/
|
|
typ = MSG_CALL;
|
|
nam = "Talk request"; /* MultiNet's TALK and/or TALK/OLD */
|
|
cmd = "TALK";
|
|
if ((p = strstri(q, " by ")) != 0) {
|
|
txt = strcat(strcpy(txt_buf, "Talk request from"), p + 3);
|
|
if ((p = strstri(p, "respond with")) != 0) {
|
|
if (*(p - 1) == '[')
|
|
*(p - 1) = '\0';
|
|
else
|
|
*p = '\0'; /* terminate */
|
|
p += (sizeof "respond with" - sizeof "");
|
|
if (*p == ':')
|
|
p++;
|
|
if (*p == ' ')
|
|
p++;
|
|
cmd = strcpy(cmd_buf, p); /* "TALK[/OLD] bar@spam" */
|
|
p = eos(cmd_buf);
|
|
if (*--p == ']')
|
|
*p = '\0';
|
|
}
|
|
} else
|
|
txt = strcat(strcpy(txt_buf, "Pardon the interruption: "), buf);
|
|
} else if (is_jnet_send) { /* sscanf(,"(%[^)])%s -%c",,,)==3 */
|
|
jnet_send:
|
|
/*
|
|
* \(SPAM\)BAR - arbitrary_message_text (from BAR@SPAM)
|
|
*/
|
|
typ = MSG_CALL;
|
|
nam = "Bitnet noise"; /* RSCS/NJE message received via JNET */
|
|
Sprintf(cmd_buf, "XYZZY %s@%s", user, node);
|
|
cmd = cmd_buf;
|
|
/*{ perhaps just vanilla SEND instead of XYZZY? }*/
|
|
Sprintf(txt_buf, "Message from %s@%s:%s", user, node,
|
|
/* "(node)user -" */
|
|
&buf[1 + strlen(node) + 1 + strlen(user) + 2 - 1]);
|
|
txt = txt_buf;
|
|
|
|
/*
|
|
* end of call recognition; anything else is none-of-the-above...
|
|
*/
|
|
} else {
|
|
other:
|
|
#endif /* SHELL */
|
|
/* arbitrary broadcast: batch job completed, system shutdown
|
|
* imminent, &c
|
|
*/
|
|
typ = MSG_OTHER;
|
|
nam = (char *) 0; /*"captured broadcast message"*/
|
|
cmd = (char *) 0;
|
|
txt = strcat(strcpy(txt_buf, "Message for you: "), buf);
|
|
#ifdef SHELL
|
|
}
|
|
/* Daemon in newmail() will append period when the text is displayed */
|
|
if ((p = eos(txt)) > txt && *--p == '.')
|
|
*p = '\0';
|
|
|
|
/* newmail() and readmail() used to assume that nam and cmd are
|
|
concatenated but that is no longer the case */
|
|
if (nam && nam != nam_buf) {
|
|
(void) strncpy(nam_buf, nam, sizeof nam_buf - 1);
|
|
nam_buf[sizeof nam_buf - 1] = '\0';
|
|
}
|
|
if (cmd && cmd != cmd_buf) {
|
|
(void) strncpy(cmd_buf, cmd, sizeof cmd_buf - 1);
|
|
cmd_buf[sizeof cmd_buf - 1] = '\0';
|
|
}
|
|
#endif /* SHELL */
|
|
/* truncate really long messages to prevent verbalize() from blowing up */
|
|
if (txt && strlen(txt) > BUFSZ - 50)
|
|
txt[BUFSZ - 50] = '\0';
|
|
|
|
msg.message_typ = typ; /* simple index */
|
|
msg.display_txt = txt; /* text for daemon to pline() */
|
|
msg.object_nam = nam; /* 'name' for mail scroll */
|
|
msg.response_cmd = cmd; /* command to spawn when scroll read */
|
|
return &msg;
|
|
}
|
|
|
|
/* filter out non-printable characters and redundant noise
|
|
*/
|
|
static void filter_brdcst(char *buf) /* called by parse_next_broadcast() */
|
|
/* in: original text; out: filtered text */
|
|
{
|
|
char c, *p, *buf_p;
|
|
|
|
/* filter the text; restrict consecutive spaces or dots to just two */
|
|
for (p = buf_p = buf; *buf_p; buf_p++) {
|
|
c = *buf_p & '\177';
|
|
if (c == ' ' || c == '\t' || c == '\n') {
|
|
if (p == buf || /* ignore leading whitespace */
|
|
(p >= buf + 2 && *(p - 1) == ' ' && *(p - 2) == ' '))
|
|
continue;
|
|
else
|
|
c = ' ';
|
|
} else if (c == '.' || c < ' ' || c == '\177') {
|
|
if (p == buf || /* skip leading beeps & such */
|
|
(p >= buf + 2 && *(p - 1) == '.' && *(p - 2) == '.'))
|
|
continue;
|
|
else
|
|
c = '.';
|
|
} else if (c == '%' && /* trim %%% OPCOM verbosity %%% */
|
|
p >= buf + 2 && *(p - 1) == '%' && *(p - 2) == '%') {
|
|
continue;
|
|
}
|
|
*p++ = c;
|
|
}
|
|
*p = '\0'; /* terminate, then strip trailing junk */
|
|
while (p > buf && (*--p == ' ' || *p == '.'))
|
|
*p = '\0';
|
|
return;
|
|
}
|
|
|
|
static char empty_string[] = "";
|
|
|
|
/* fetch the text of a captured broadcast, then mangle and decipher it
|
|
*/
|
|
struct mail_info *parse_next_broadcast(void) /* called by ckmailstatus(mail.c) */
|
|
{
|
|
short length, msg_type;
|
|
$DESCRIPTOR(message, empty_string); /* string descriptor for buf[] */
|
|
struct mail_info *result = 0;
|
|
/* messages could actually be longer; let long ones be truncated */
|
|
char buf[255 + 1];
|
|
|
|
message.dsc$a_pointer = buf, message.dsc$w_length = sizeof buf - 1;
|
|
msg_type = length = 0;
|
|
smg$get_broadcast_message(&pasteboard_id, &message, &length, &msg_type);
|
|
if (msg_type == MSG$_TRMBRDCST) {
|
|
buf[length] = '\0';
|
|
filter_brdcst(buf); /* mask non-printable characters */
|
|
result = parse_brdcst(buf); /* do the real work */
|
|
} else if (msg_type == MSG$_TRMHANGUP) {
|
|
(void) gsignal(SIGHUP);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* spit out any pending broadcast messages whenever we leave
|
|
*/
|
|
static void flush_broadcasts(void) /* called from disable_broadcast_trapping() */
|
|
{
|
|
if (broadcasts > 0) {
|
|
short len, typ;
|
|
$DESCRIPTOR(msg_dsc, empty_string);
|
|
char buf[512 + 1];
|
|
|
|
msg_dsc.dsc$a_pointer = buf, msg_dsc.dsc$w_length = sizeof buf - 1;
|
|
raw_print(""); /* print at least one line for wait_synch() */
|
|
do {
|
|
typ = len = 0;
|
|
smg$get_broadcast_message(&pasteboard_id, &msg_dsc, &len, &typ);
|
|
if (typ == MSG$_TRMBRDCST)
|
|
buf[len] = '\0', raw_print(buf);
|
|
} while (--broadcasts);
|
|
wait_synch(); /* prompt with "Hit return to continue: " */
|
|
}
|
|
}
|
|
|
|
/* AST routine called when terminal's associated mailbox receives a message
|
|
*/
|
|
/*ARGSUSED*/
|
|
static void
|
|
broadcast_ast(int dummy UNUSED) /* called asynchronously by terminal driver */
|
|
{
|
|
broadcasts++;
|
|
}
|
|
|
|
/* initialize the broadcast manipulation code; SMG makes this easy
|
|
*/
|
|
unsigned long
|
|
init_broadcast_trapping(void) /* called by setftty() [once only] */
|
|
{
|
|
unsigned long sts, preserve_screen_flag = 1;
|
|
|
|
/* we need a pasteboard to pass to the broadcast setup/teardown routines */
|
|
sts = smg$create_pasteboard(&pasteboard_id, 0, 0, 0,
|
|
&preserve_screen_flag);
|
|
if (!vms_ok(sts)) {
|
|
errno = EVMSERR, vaxc$errno = sts;
|
|
raw_print("");
|
|
perror("?can't create SMG pasteboard for broadcast trapping");
|
|
wait_synch();
|
|
broadcasts = -1; /* flag that trapping is currently broken */
|
|
}
|
|
return sts;
|
|
}
|
|
|
|
/* set up the terminal driver to deliver $brkthru data to a mailbox device
|
|
*/
|
|
unsigned long
|
|
enable_broadcast_trapping(void) /* called by setftty() */
|
|
{
|
|
unsigned long sts = 1;
|
|
|
|
if (broadcasts >= 0) { /* (-1 => no pasteboard, so don't even try) */
|
|
/* register callback routine to be triggered when broadcasts arrive */
|
|
/* Note side effect: also intercepts hangup notification. */
|
|
/* Another note: TMPMBX privilege is required. */
|
|
sts = smg$set_broadcast_trapping(&pasteboard_id, broadcast_ast, 0);
|
|
if (!vms_ok(sts)) {
|
|
errno = EVMSERR, vaxc$errno = sts;
|
|
raw_print("");
|
|
perror("?can't enable broadcast trapping");
|
|
wait_synch();
|
|
}
|
|
}
|
|
return sts;
|
|
}
|
|
|
|
/* return to 'normal'; $brkthru data goes straight to the terminal
|
|
*/
|
|
unsigned long
|
|
disable_broadcast_trapping(void) /* called by settty() */
|
|
{
|
|
unsigned long sts = 1;
|
|
|
|
if (broadcasts >= 0) {
|
|
/* disable trapping; releases associated MBX so that SPAWN can work */
|
|
sts = smg$disable_broadcast_trapping(&pasteboard_id);
|
|
if (!vms_ok(sts))
|
|
errno = EVMSERR, vaxc$errno = sts;
|
|
flush_broadcasts(); /* don't hold on to any buffered ones */
|
|
}
|
|
return sts;
|
|
}
|
|
|
|
#else /* MAIL */
|
|
|
|
/* simple stubs for non-mail configuration */
|
|
unsigned long
|
|
init_broadcast_trapping(void)
|
|
{
|
|
return 1;
|
|
}
|
|
unsigned long
|
|
enable_broadcast_trapping(void)
|
|
{
|
|
return 1;
|
|
}
|
|
unsigned long
|
|
disable_broadcast_trapping(void)
|
|
{
|
|
return 1;
|
|
}
|
|
struct mail_info *
|
|
parse_next_broadcast(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
#endif /* MAIL */
|
|
|
|
/*----------------------------------------------------------------------*/
|
|
|
|
#ifdef TEST_DRIVER
|
|
/* (Take parse_next_broadcast for a spin. :-) */
|
|
|
|
volatile int broadcasts = 0;
|
|
|
|
void
|
|
newmail(foo)
|
|
struct mail_info *foo;
|
|
{
|
|
#define STRING(s) ((s) ? (s) : "<null>")
|
|
printf("\n\
|
|
message type = %d\n\
|
|
display text = \"%s\"\n\
|
|
object name = \"%.*s\"\n\
|
|
response cmd = \"%s\"\n\
|
|
",
|
|
foo->message_typ, STRING(foo->display_txt),
|
|
(foo->object_nam && foo->response_cmd)
|
|
? (foo->response_cmd - foo->object_nam - 1)
|
|
: strlen(STRING(foo->object_nam)),
|
|
STRING(foo->object_nam), STRING(foo->response_cmd));
|
|
#undef STRING
|
|
}
|
|
|
|
void
|
|
ckmailstatus(void)
|
|
{
|
|
struct mail_info *brdcst, *parse_next_broadcast();
|
|
|
|
while (broadcasts > 0) { /* process all trapped broadcasts [until] */
|
|
broadcasts--;
|
|
if ((brdcst = parse_next_broadcast()) != 0) {
|
|
newmail(brdcst);
|
|
break; /* only handle one real message at a time */
|
|
} else
|
|
printf("\n--< non-broadcast encountered >--\n");
|
|
}
|
|
}
|
|
|
|
int
|
|
main(int argc UNUSED, char *argv[] UNUSED)
|
|
{
|
|
char dummy[BUFSIZ];
|
|
|
|
init_broadcast_trapping();
|
|
enable_broadcast_trapping();
|
|
for (;;) {
|
|
ckmailstatus();
|
|
printf("> "), fflush(stdout); /* issue a prompt */
|
|
if (!gets(dummy))
|
|
break; /* wait for a response */
|
|
}
|
|
disable_broadcast_trapping();
|
|
return 1;
|
|
}
|
|
|
|
void
|
|
panic(char *s)
|
|
{
|
|
raw_print(s);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
void
|
|
raw_print(char *s)
|
|
{
|
|
puts(s);
|
|
fflush(stdout);
|
|
}
|
|
|
|
void
|
|
wait_synch(void)
|
|
{
|
|
char dummy[BUFSIZ];
|
|
|
|
printf("\nPress <return> to continue: ");
|
|
fflush(stdout);
|
|
(void) gets(dummy);
|
|
}
|
|
#endif /* TEST_DRIVER */
|
|
|
|
/*vmsmail.c*/
|