1 /* NetHack 3.7	vmsmail.c	$NHDT-Date: 1596498307 2020/08/03 23:45:07 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.11 $ */
2 /* Copyright (c) Robert Patrick Rankin, 1991.                     */
3 /* NetHack may be freely redistributed.  See license for details. */
4 
5 #include "config.h"
6 #include "mail.h"
7 
8 /* lint supression due to lack of extern.h */
9 unsigned long init_broadcast_trapping(void);
10 unsigned long enable_broadcast_trapping(void);
11 unsigned long disable_broadcast_trapping(void);
12 struct mail_info *parse_next_broadcast(void);
13 
14 #ifdef MAIL
15 #include "wintype.h"
16 #include "winprocs.h"
17 #include <ctype.h>
18 #include <descrip.h>
19 #include <errno.h>
20 #ifndef __GNUC__
21 #include <msgdef.h>
22 #else
23 #define MSG$_TRMHANGUP 6
24 #define MSG$_TRMBRDCST 83
25 #endif /*__GNUC__*/
26 #include <signal.h>
27 /* #include <string.h> */
28 #define vms_ok(sts) ((sts) & 1)
29 
30 static struct mail_info *parse_brdcst(char *);
31 static void filter_brdcst(char *);
32 static void flush_broadcasts(void);
33 static void broadcast_ast(int);
34 extern char *eos(char *);
35 extern char *strstri(const char *, const char *);
36 extern int strncmpi(const char *, const char *, int);
37 
38 extern size_t strspn(const char *, const char *);
39 #ifndef __DECC
40 extern int VDECL(sscanf, (const char *, const char *, ...));
41 #endif
42 extern unsigned long smg$create_pasteboard(), smg$get_broadcast_message(),
43     smg$set_broadcast_trapping(), smg$disable_broadcast_trapping();
44 
45 extern volatile int broadcasts; /* defining declaration in mail.c */
46 
47 static long pasteboard_id = 0; /* SMG's magic cookie */
48 
49 /*
50  * Mail (et al) overview:
51  *
52  *      When a broadcast is asynchronously captured, a volatile counter
53  * ('broadcasts') is incremented.  Each player turn, ckmailstatus() polls
54  * the counter and calls parse_next_broadcast() if it's positive; this
55  * returns some display text, object name, and response command, which is
56  * passed to newmail().  Routine newmail() generates a mail-daemon monster
57  * who approaches the character, "speaks" the display text, and delivers
58  * a scroll of mail pre-named to the object name; the response command is
59  * also attached to the scroll's oextra->omailcmd field.
60  * Unrecognized broadcasts result in the mail-daemon
61  * arriving and announcing the display text, but no scroll being created.
62  * If SHELL is undefined, then all broadcasts are treated as 'other'; since
63  * no subproceses are allowed, there'd be no way to respond to the scroll.
64  *
65  *      When a scroll of mail is read by the character, readmail() extracts
66  * the command string and uses it for the default when prompting the
67  * player for a system command to spawn.  The player may enter any command
68  * he or she chooses, or just <return> to accept the default or <escape> to
69  * avoid executing any command.  If the command is "SPAWN", a regular shell
70  * escape to DCL is performed; otherwise, the indicated single command is
71  * spawned.  Either way, NetHack resumes play when the subprocess terminates
72  * or explicitly reattaches to its parent.
73  *
74  * Broadcast parsing:
75  *
76  *      The following broadcast messages are [attempted to be] recognized:
77  *    text fragment           name for scroll         default command
78  *      New mail                VMSmail                 MAIL
79  *      New ALL-IN-1 MAIL       A1mail                  A1M
80  *      Software Tools mail     STmail                  MSG [+folder]
81  *      MM mail                 MMmail                  MM
82  *      WPmail: New mail        WPmail                  OFFICE/MAIL
83  *      **M400 mail             M400mail                M400
84  *      " mail", ^"mail "       unknown mail            SPAWN
85  *      " phoning"              Phone call              PHONE ANSWER
86  *      talk-daemon...by...foo  Talk request            TALK[/OLD] foo@bar
87  *      (node)user -            Bitnet noise            XYZZY user@node
88  * Anything else results in just the message text being passed along, no
89  * scroll of mail so consequently no command to execute when scroll read.
90  * The user can set up ``$ XYZZY :== SEND'' prior to invoking NetHack if
91  * vanilla JNET responses to Bitnet messages are prefered.
92  *
93  *      Static return buffers are used because only one broadcast gets
94  * processed at a time, and the essential information in each one is
95  * either displayed and discarded or copied into a scroll-of-mail object.
96  *
97  *      The test driver code below can be used to check out potential new
98  * entries without rebuilding NetHack itself.  CC/DEFINE="TEST_DRIVER"
99  * Link it with hacklib.obj or nethack.olb/incl=hacklib (not nethack/lib).
100  */
101 
102 static struct mail_info msg;  /* parse_*()'s return buffer */
103 static char nam_buf[63],      /* maximum onamelth, size of ONAME(object) */
104             cmd_buf[99],      /* arbitrary */
105             txt_buf[255 + 1]; /* same size as used for message buf[] */
106 
107 /* try to decipher and categorize broadcast message text
108 */
109 static struct mail_info *
parse_brdcst(buf)110 parse_brdcst(buf) /* called by parse_next_broadcast() */
111 char *buf;        /* input: filtered broadcast text */
112 {
113     int typ;
114     char *txt;
115     const char *nam, *cmd;
116 #ifdef SHELL /* only parse if spawned commands are enabled */
117     register char *p, *q;
118     boolean is_jnet_send;
119     char user[127 + 1], node[127 + 1], sentinel;
120 
121     /* Check these first; otherwise, their arbitrary text would enable
122         easy spoofing of some other message patterns.  Unfortunately,
123         any home-grown broadcast delivery program poses a similar risk. */
124     if (!strncmpi(buf, "reply received", 14))
125         goto other;
126     is_jnet_send = (sscanf(buf, "(%[^)])%s -%c", node, user, &sentinel) == 3);
127     if (is_jnet_send)
128         goto jnet_send;
129 
130     /* scan the text more or less by brute force */
131     if ((q = strstri(buf, " mail")) != 0 || /* all known mail broadcasts */
132         !strncmpi(q = buf, "mail ", 5)) {   /* unexpected alternative */
133         typ = MSG_MAIL;
134         p = strstri(q, " from");
135         txt = p ? strcat(strcpy(txt_buf, "Mail for you"), p) : (char *) 0;
136 
137         if (!strncmpi(buf, "new mail", 8)) {
138             /*
139              * New mail [on node FOO] from [SPAM::]BAR
140              *  [\"personal_name\"] [\(HH:MM:SS\)]
141              */
142             nam = "VMSmail"; /* assume VMSmail */
143             cmd = "MAIL";
144             if (txt && (p = strrchr(txt, '(')) > txt && /* discard time */
145                 (--p, strspn(p, "0123456789( :.)") == strlen(p)))
146                 *p = '\0';
147         } else if (!strncmpi(buf, "new all-in-1", 12)) {
148             int i;
149             /*
150              * New ALL-IN-1 MAIL message [on node FOO] from Personal Name
151              * \(BAR@SPAM\) [\(DD-MMM-YYYY HH:MM:SS\)]
152              */
153             nam = "A1mail";
154             cmd = "A1M";
155             if (txt && (p = strrchr(txt, '(')) > txt
156                 && /* discard date+time */
157                 sscanf(p - 1, " (%*d-%*[^-]-%*d %*d:%*d:%d) %c", &i,
158                        &sentinel) == 1)
159                 *--p = '\0';
160         } else if (!strncmpi(buf, "software tools", 14)) {
161             /*
162              * Software Tools mail has arrived on FOO from \'BAR\' [in SPAM]
163              */
164             nam = "STmail";
165             cmd = "MSG";
166             if (txt && (p = strstri(p, " in ")) != 0) /* specific folder */
167                 cmd = strcat(strcpy(cmd_buf, "MSG +"), p + 4);
168         } else if (q - 2 >= buf && !strncmpi(q - 2, "mm", 2)) {
169             /*
170              * {MultiNet\ |PMDF\/}MM mail has arrived on FOO from BAR\n
171              * [Subject: subject_text] (PMDF only)
172              */
173             nam = "MMmail"; /* MultiNet's version of MM */
174             cmd = "MM";     /*{ perhaps "MM READ"? }*/
175         } else if (!strncmpi(buf, "wpmail:", 7)) {
176             /*
177              * WPmail: New mail from BAR.  subject_text
178              */
179             nam = "WPmail"; /* WordPerfect [sic] Office */
180             cmd = "OFFICE/MAIL";
181         } else if (!strncmpi(buf, "**m400 mail", 7)) {
182             /*
183              * **M400 mail waiting**
184              */
185             nam = "M400mail"; /* Messenger 400 [not seen] */
186             cmd = "M400";
187         } else {
188             /* not recognized, but presumed to be mail */
189             nam = "unknown mail";
190             cmd = "SPAWN";    /* generic escape back to DCL */
191             txt = (char *) 0; /* don't rely on "from" info here */
192         }
193 
194         if (!txt)
195             txt = strcat(strcpy(txt_buf, "Mail for you: "), buf);
196 
197     /*
198      * end of mail recognition; now check for call-type interruptions...
199      */
200     } else if ((q = strstri(buf, " phoning")) != 0) {
201         /*
202          * BAR is phoning you [on FOO] \(HH:MM:SS\)
203          */
204         typ = MSG_CALL;
205         nam = "Phone call";
206         cmd = "PHONE ANSWER";
207         if (!strncmpi(q + 8, " you", 4))
208             q += (8 + 4), *q = '\0';
209         txt = strcat(strcpy(txt_buf, "Do you hear ringing?  "), buf);
210     } else if ((q = strstri(buf, " talk-daemon")) != 0
211                || (q = strstri(buf, " talk_daemon")) != 0) {
212         /*
213          * Message from TALK-DAEMON@FOO at HH:MM:SS\n
214          * Connection request by BAR@SPAM\n
215          * \[Respond with: TALK[/OLD] BAR@SPAM\]
216          */
217         typ = MSG_CALL;
218         nam = "Talk request"; /* MultiNet's TALK and/or TALK/OLD */
219         cmd = "TALK";
220         if ((p = strstri(q, " by ")) != 0) {
221             txt = strcat(strcpy(txt_buf, "Talk request from"), p + 3);
222             if ((p = strstri(p, "respond with")) != 0) {
223                 if (*(p - 1) == '[')
224                     *(p - 1) = '\0';
225                 else
226                     *p = '\0'; /* terminate */
227                 p += (sizeof "respond with" - sizeof "");
228                 if (*p == ':')
229                     p++;
230                 if (*p == ' ')
231                     p++;
232                 cmd = strcpy(cmd_buf, p); /* "TALK[/OLD] bar@spam" */
233                 p = eos(cmd_buf);
234                 if (*--p == ']')
235                     *p = '\0';
236             }
237         } else
238             txt = strcat(strcpy(txt_buf, "Pardon the interruption: "), buf);
239     } else if (is_jnet_send) { /* sscanf(,"(%[^)])%s -%c",,,)==3 */
240     jnet_send:
241         /*
242          *      \(SPAM\)BAR - arbitrary_message_text (from BAR@SPAM)
243          */
244         typ = MSG_CALL;
245         nam = "Bitnet noise"; /* RSCS/NJE message received via JNET */
246         Sprintf(cmd_buf, "XYZZY %s@%s", user, node);
247         cmd = cmd_buf;
248         /*{ perhaps just vanilla SEND instead of XYZZY? }*/
249         Sprintf(txt_buf, "Message from %s@%s:%s", user, node,
250                 /* "(node)user -" */
251                 &buf[1 + strlen(node) + 1 + strlen(user) + 2 - 1]);
252         txt = txt_buf;
253 
254     /*
255      * end of call recognition; anything else is none-of-the-above...
256      */
257     } else {
258     other:
259 #endif  /* SHELL */
260         /* arbitrary broadcast: batch job completed, system shutdown
261          * imminent, &c
262          */
263         typ = MSG_OTHER;
264         nam = (char *) 0; /*"captured broadcast message"*/
265         cmd = (char *) 0;
266         txt = strcat(strcpy(txt_buf, "Message for you: "), buf);
267 #ifdef SHELL
268     }
269     /* Daemon in newmail() will append period when the text is displayed */
270     if ((p = eos(txt)) > txt && *--p == '.')
271         *p = '\0';
272 
273     /* newmail() and readmail() used to assume that nam and cmd are
274        concatenated but that is no longer the case */
275     if (nam && nam != nam_buf) {
276         (void) strncpy(nam_buf, nam, sizeof nam_buf - 1);
277         nam_buf[sizeof nam_buf - 1] = '\0';
278     }
279     if (cmd && cmd != cmd_buf) {
280         (void) strncpy(cmd_buf, cmd, sizeof cmd_buf - 1);
281         cmd_buf[sizeof cmd_buf - 1] = '\0';
282     }
283 #endif /* SHELL */
284     /* truncate really long messages to prevent verbalize() from blowing up */
285     if (txt && strlen(txt) > BUFSZ - 50)
286         txt[BUFSZ - 50] = '\0';
287 
288     msg.message_typ = typ;  /* simple index */
289     msg.display_txt = txt;  /* text for daemon to pline() */
290     msg.object_nam = nam;   /* 'name' for mail scroll */
291     msg.response_cmd = cmd; /* command to spawn when scroll read */
292     return &msg;
293 }
294 
295 /* filter out non-printable characters and redundant noise
296 */
filter_brdcst(buf)297 static void filter_brdcst(buf) /* called by parse_next_broadcast() */
298 register char *buf;            /* in: original text; out: filtered text */
299 {
300     register char c, *p, *buf_p;
301 
302     /* filter the text; restrict consecutive spaces or dots to just two */
303     for (p = buf_p = buf; *buf_p; buf_p++) {
304         c = *buf_p & '\177';
305         if (c == ' ' || c == '\t' || c == '\n') {
306             if (p == buf || /* ignore leading whitespace */
307                 (p >= buf + 2 && *(p - 1) == ' ' && *(p - 2) == ' '))
308                 continue;
309             else
310                 c = ' ';
311         } else if (c == '.' || c < ' ' || c == '\177') {
312             if (p == buf || /* skip leading beeps & such */
313                 (p >= buf + 2 && *(p - 1) == '.' && *(p - 2) == '.'))
314                 continue;
315             else
316                 c = '.';
317         } else if (c == '%' && /* trim %%% OPCOM verbosity %%% */
318                    p >= buf + 2 && *(p - 1) == '%' && *(p - 2) == '%') {
319             continue;
320         }
321         *p++ = c;
322     }
323     *p = '\0'; /* terminate, then strip trailing junk */
324     while (p > buf && (*--p == ' ' || *p == '.'))
325         *p = '\0';
326     return;
327 }
328 
329 static char empty_string[] = "";
330 
331 /* fetch the text of a captured broadcast, then mangle and decipher it
332  */
parse_next_broadcast()333 struct mail_info *parse_next_broadcast() /* called by ckmailstatus(mail.c) */
334 {
335     short length, msg_type;
336     $DESCRIPTOR(message, empty_string); /* string descriptor for buf[] */
337     struct mail_info *result = 0;
338     /* messages could actually be longer; let long ones be truncated */
339     char buf[255 + 1];
340 
341     message.dsc$a_pointer = buf, message.dsc$w_length = sizeof buf - 1;
342     msg_type = length = 0;
343     smg$get_broadcast_message(&pasteboard_id, &message, &length, &msg_type);
344     if (msg_type == MSG$_TRMBRDCST) {
345         buf[length] = '\0';
346         filter_brdcst(buf);         /* mask non-printable characters */
347         result = parse_brdcst(buf); /* do the real work */
348     } else if (msg_type == MSG$_TRMHANGUP) {
349         (void) gsignal(SIGHUP);
350     }
351     return result;
352 }
353 
354 /* spit out any pending broadcast messages whenever we leave
355  */
flush_broadcasts()356 static void flush_broadcasts() /* called from disable_broadcast_trapping() */
357 {
358     if (broadcasts > 0) {
359         short len, typ;
360         $DESCRIPTOR(msg_dsc, empty_string);
361         char buf[512 + 1];
362 
363         msg_dsc.dsc$a_pointer = buf, msg_dsc.dsc$w_length = sizeof buf - 1;
364         raw_print(""); /* print at least one line for wait_synch() */
365         do {
366             typ = len = 0;
367             smg$get_broadcast_message(&pasteboard_id, &msg_dsc, &len, &typ);
368             if (typ == MSG$_TRMBRDCST)
369                 buf[len] = '\0', raw_print(buf);
370         } while (--broadcasts);
371         wait_synch(); /* prompt with "Hit return to continue: " */
372     }
373 }
374 
375 /* AST routine called when terminal's associated mailbox receives a message
376  */
377 /*ARGSUSED*/
378 static void
broadcast_ast(dummy)379 broadcast_ast(dummy) /* called asynchronously by terminal driver */
380 int dummy UNUSED;
381 {
382     broadcasts++;
383 }
384 
385 /* initialize the broadcast manipulation code; SMG makes this easy
386 */
387 unsigned long
init_broadcast_trapping()388 init_broadcast_trapping() /* called by setftty() [once only] */
389 {
390     unsigned long sts, preserve_screen_flag = 1;
391 
392     /* we need a pasteboard to pass to the broadcast setup/teardown routines */
393     sts = smg$create_pasteboard(&pasteboard_id, 0, 0, 0,
394                                 &preserve_screen_flag);
395     if (!vms_ok(sts)) {
396         errno = EVMSERR, vaxc$errno = sts;
397         raw_print("");
398         perror("?can't create SMG pasteboard for broadcast trapping");
399         wait_synch();
400         broadcasts = -1; /* flag that trapping is currently broken */
401     }
402     return sts;
403 }
404 
405 /* set up the terminal driver to deliver $brkthru data to a mailbox device
406  */
407 unsigned long
enable_broadcast_trapping()408 enable_broadcast_trapping() /* called by setftty() */
409 {
410     unsigned long sts = 1;
411 
412     if (broadcasts >= 0) { /* (-1 => no pasteboard, so don't even try) */
413         /* register callback routine to be triggered when broadcasts arrive */
414         /* Note side effect:  also intercepts hangup notification. */
415         /* Another note:  TMPMBX privilege is required. */
416         sts = smg$set_broadcast_trapping(&pasteboard_id, broadcast_ast, 0);
417         if (!vms_ok(sts)) {
418             errno = EVMSERR, vaxc$errno = sts;
419             raw_print("");
420             perror("?can't enable broadcast trapping");
421             wait_synch();
422         }
423     }
424     return sts;
425 }
426 
427 /* return to 'normal'; $brkthru data goes straight to the terminal
428  */
429 unsigned long
disable_broadcast_trapping()430 disable_broadcast_trapping() /* called by settty() */
431 {
432     unsigned long sts = 1;
433 
434     if (broadcasts >= 0) {
435         /* disable trapping; releases associated MBX so that SPAWN can work */
436         sts = smg$disable_broadcast_trapping(&pasteboard_id);
437         if (!vms_ok(sts))
438             errno = EVMSERR, vaxc$errno = sts;
439         flush_broadcasts(); /* don't hold on to any buffered ones */
440     }
441     return sts;
442 }
443 
444 #else  /* MAIL */
445 
446 /* simple stubs for non-mail configuration */
447 unsigned long
init_broadcast_trapping()448 init_broadcast_trapping()
449 {
450     return 1;
451 }
452 unsigned long
enable_broadcast_trapping()453 enable_broadcast_trapping()
454 {
455     return 1;
456 }
457 unsigned long
disable_broadcast_trapping()458 disable_broadcast_trapping()
459 {
460     return 1;
461 }
462 struct mail_info *
parse_next_broadcast()463 parse_next_broadcast()
464 {
465     return 0;
466 }
467 
468 #endif /* MAIL */
469 
470 /*----------------------------------------------------------------------*/
471 
472 #ifdef TEST_DRIVER
473 /* (Take parse_next_broadcast for a spin. :-) */
474 
475 volatile int broadcasts = 0;
476 
477 void
newmail(foo)478 newmail(foo)
479 struct mail_info *foo;
480 {
481 #define STRING(s) ((s) ? (s) : "<null>")
482     printf("\n\
483   message type = %d\n\
484   display text = \"%s\"\n\
485   object name  = \"%.*s\"\n\
486   response cmd = \"%s\"\n\
487 ",
488            foo->message_typ, STRING(foo->display_txt),
489            (foo->object_nam && foo->response_cmd)
490                ? (foo->response_cmd - foo->object_nam - 1)
491                : strlen(STRING(foo->object_nam)),
492            STRING(foo->object_nam), STRING(foo->response_cmd));
493 #undef STRING
494 }
495 
496 void
ckmailstatus()497 ckmailstatus()
498 {
499     struct mail_info *brdcst, *parse_next_broadcast();
500 
501     while (broadcasts > 0) { /* process all trapped broadcasts [until] */
502         broadcasts--;
503         if ((brdcst = parse_next_broadcast()) != 0) {
504             newmail(brdcst);
505             break; /* only handle one real message at a time */
506         } else
507             printf("\n--< non-broadcast encountered >--\n");
508     }
509 }
510 
511 int
main()512 main()
513 {
514     char dummy[BUFSIZ];
515 
516     init_broadcast_trapping();
517     enable_broadcast_trapping();
518     for (;;) {
519         ckmailstatus();
520         printf("> "), fflush(stdout); /* issue a prompt */
521         if (!gets(dummy))
522             break; /* wait for a response */
523     }
524     disable_broadcast_trapping();
525     return 1;
526 }
527 
528 void
panic(s)529 panic(s)
530 char *s;
531 {
532     raw_print(s);
533     exit(EXIT_FAILURE);
534 }
535 
536 void
raw_print(s)537 raw_print(s)
538 char *s;
539 {
540     puts(s);
541     fflush(stdout);
542 }
543 
544 void
wait_synch()545 wait_synch()
546 {
547     char dummy[BUFSIZ];
548 
549     printf("\nPress <return> to continue: ");
550     fflush(stdout);
551     (void) gets(dummy);
552 }
553 #endif /* TEST_DRIVER */
554 
555 /*vmsmail.c*/
556