root/1.8.3/tags/p6/src/extmail.c

Revision 1167, 79.8 KB (checked in by shawnw, 13 months ago)

Merge devel into trunk for p6 release

Line 
1/**
2 * \file extmail.c
3 *
4 * \brief The PennMUSH built-in mail system.
5 *
6 * \verbatim
7 *---------------------------------------------------------------
8 * extmail.c - Javelin's improved @mail system
9 * Based on Amberyl's linked list mailer
10 *
11 * Summary of mail command syntax:
12 * Sending:
13 *   @mail[/sendswitch] player-list = message
14 *     sendswitches: /silent, /urgent
15 *     player-list is a space-separated list of players, aliases, or msg#'s
16 *     to reply to. Players can be names or dbrefs. Aliases start with *
17 * Reading/Handling:
18 *   @mail[/readswitch] [msg-list [= target]]
19 *     With no readswitch, @mail reads msg-list (same as /read)
20 *     With no readswitch and no msg-list, @mail lists all messages (/list)
21 *     readswitches: /list, /read, /fwd (requires target list of players
22 *      to forward messages to), /file (requires target folder to file
23 *      to), /tag, /untag, /clear, /unclear, /purge (no msg-list),
24 *      /count
25 *     Assumes messages in current folder, set by @folder or @mail/folder
26 *     msg-list can be one of: a message number, a message range,
27 *     (2-3, 4-, -6), sender references (*player), date comparisons
28 *     (~0, >2, <5), or the strings "urgent", "tagged", "cleared",
29 *     "read", "unread", "all", or "folder"
30 *     You can also use 1:2 (folder 1, message 2) and 1:2-3 for ranges.
31 * Admin stuff:
32 *   @mail[/switch] [player]
33 *     Switches include: nuke (used to be "purge"), [efd]stats, debug
34 *
35 * THEORY OF OPERATION:
36 *  Prior to pl11, mail was an unsorted linked list. When mail was sent,
37 * it was added onto the end. To read mail, you scanned the whole list.
38 * This is still how origmail.c works.
39 *  As of pl11, extmail.c maintains mail as a sorted linked list, sorted
40 * by recipient and order of receipt. This makes sending mail less
41 * efficient (because you have to scan the list to figure out where to
42 * insert), but reading/checking/deleting more efficient,
43 * because once you've found where the player's mail starts, you just
44 * read from there.
45 *  That wouldn't be so exciting unless there was a fast way to find
46 * where a player's mail chain started. Fortunately, there is. We
47 * record that information for connected players when they connect,
48 * on their descriptor. So, when connected players do reading/etc,
49 * it's O(1). Sending to a connected player is O(1). Sending to an
50 * unconnected player still requires scanning (O(n)), but you send once,
51 * and read/list/delete etc, multiple times.
52 *  And just to make the sending to disconnected players faster,
53 * instead of scanning the whole maildb to find the insertion point,
54 * we start the scan from the chain of the connected player with the
55 * closest db# to the target player. This scales up very well.
56 *--------------------------------------------------------------------
57 * \endverbatim
58 */
59
60#include "config.h"
61#include "copyrite.h"
62
63#ifdef I_SYS_TIME
64#include <sys/time.h>
65#ifdef TIME_WITH_SYS_TIME
66#include <time.h>
67#endif
68#else
69#include <time.h>
70#endif
71#include <ctype.h>
72#ifdef I_SYS_TYPES
73#include <sys/types.h>
74#endif
75#include <string.h>
76#ifdef HAVE_STDINT_H
77#include <stdint.h>
78#endif
79
80#include "conf.h"
81#include "externs.h"
82#include "mushdb.h"
83#include "dbdefs.h"
84#include "match.h"
85#include "extmail.h"
86#include "function.h"
87#include "malias.h"
88#include "attrib.h"
89#include "parse.h"
90#include "mymalloc.h"
91#include "ansi.h"
92#include "pueblo.h"
93#include "flags.h"
94#include "log.h"
95#include "lock.h"
96#include "command.h"
97#include "dbio.h"
98#include "confmagic.h"
99
100
101extern int do_convtime(const char *str, struct tm *ttm);        /* funtime.c */
102
103static void do_mail_flags
104  (dbref player, const char *msglist, mail_flag flag, bool negate);
105static char *mail_list_time(const char *the_time, bool flag);
106static MAIL *mail_fetch(dbref player, int num);
107static MAIL *real_mail_fetch(dbref player, int num, int folder);
108static MAIL *mailfun_fetch(dbref player, int nargs, char *arg1, char *arg2);
109static void count_mail(dbref player,
110                       int folder, int *rcount, int *ucount, int *ccount);
111static int real_send_mail(dbref player,
112                          dbref target, char *subject, char *message,
113                          mail_flag flags, int silent, int nosig);
114static void send_mail(dbref player,
115                      dbref target, char *subject, char *message,
116                      mail_flag flags, int silent, int nosig);
117static int send_mail_alias(dbref player,
118                           char *aname, char *subject,
119                           char *message, mail_flag flags, int silent,
120                           int nosig);
121static void filter_mail(dbref from, dbref player, char *subject,
122                        char *message, int mailnumber, mail_flag flags);
123static MAIL *find_insertion_point(dbref player);
124static int get_folder_number(dbref player, char *name);
125static char *get_folder_name(dbref player, int fld);
126static int player_folder(dbref player);
127static int parse_folder(dbref player, char *folder_string);
128static int mail_match(dbref player, MAIL *mp, struct mail_selector ms, int num);
129static int parse_msglist
130  (const char *msglist, struct mail_selector *ms, dbref player);
131static int parse_message_spec
132  (dbref player, const char *s, int *msglow, int *msghigh, int *folder);
133static char *status_chars(MAIL *mp);
134static char *status_string(MAIL *mp);
135static int sign(int x);
136static char *get_message(MAIL *mp);
137static unsigned char *get_compressed_message(MAIL *mp);
138static char *get_subject(MAIL *mp);
139static char *get_sender(MAIL *mp, int full);
140static int was_sender(dbref player, MAIL *mp);
141
142MAIL *maildb;            /**< The head of the mail list */
143MAIL *tail_ptr;          /**< The end of the mail list */
144
145slab *mail_slab; /**< slab for 'struct mail' allocations */
146
147#define HEAD  maildb     /**< The head of the mail list */
148#define TAIL  tail_ptr   /**< The end of the mail list */
149
150/** A line of...dashes! */
151#define DASH_LINE  \
152  "-----------------------------------------------------------------------------"
153
154int mdb_top = 0;                /**< total number of messages in mail db */
155
156/*-------------------------------------------------------------------------*
157 *   User mail functions (these are called from game.c)
158 *
159 * do_mail - cases without a /switch.
160 * do_mail_send - sending mail
161 * do_mail_read - read messages
162 * do_mail_list - list messages
163 * do_mail_flags - tagging, untagging, clearing, unclearing of messages
164 * do_mail_file - files messages into a new folder
165 * do_mail_fwd - forward messages to another player(s)
166 * do_mail_count - count messages
167 * do_mail_purge - purge cleared messages
168 * do_mail_change_folder - change current folder
169 * do_mail_unfolder - remove a folder name from MAILFOLDERS
170 * do_mail_subject - set the current mail subject
171 *-------------------------------------------------------------------------*/
172
173/* Return the uncompressed text of a @mail in a static buffer */
174static char *
175get_message(MAIL *mp)
176{
177  static char text[BUFFER_LEN * 2];
178  unsigned char tbuf[BUFFER_LEN * 2];
179
180  if (!mp)
181    return NULL;
182
183  chunk_fetch(mp->msgid, tbuf, sizeof tbuf);
184  strcpy(text, uncompress(tbuf));
185  return text;
186}
187
188/* Return the compressed text of a @mail in a static buffer */
189static unsigned char *
190get_compressed_message(MAIL *mp)
191{
192  static unsigned char text[BUFFER_LEN * 2];
193
194  if (!mp)
195    return NULL;
196
197  chunk_fetch(mp->msgid, (unsigned char *) text, sizeof text);
198  return text;
199}
200
201/* Return the subject of a mail message, or (no subject) */
202static char *
203get_subject(MAIL *mp)
204{
205  static char sbuf[SUBJECT_LEN + 1];
206  char *p;
207  if (mp->subject) {
208    strncpy(sbuf, uncompress(mp->subject), SUBJECT_LEN);
209    sbuf[SUBJECT_LEN] = '\0';
210    /* Stop at a return or a tab */
211    for (p = sbuf; *p; p++) {
212      if ((*p == '\r') || (*p == '\n') || (*p == '\t')) {
213        *p = '\0';
214        break;
215      }
216      if (!isprint((unsigned char) *p)) {
217        *p = ' ';
218      }
219    }
220  } else
221    strcpy(sbuf, T("(no subject)"));
222  return sbuf;
223}
224
225/* Return the name of the mail sender. */
226static char *
227get_sender(MAIL *mp, int full)
228{
229  static char tbuf1[BUFFER_LEN], *bp;
230  bp = tbuf1;
231  if (!GoodObject(mp->from))
232    safe_str("!Purged!", tbuf1, &bp);
233  else if (!was_sender(mp->from, mp))
234    safe_str("!Purged!", tbuf1, &bp);
235  else if (IsPlayer(mp->from) || !full)
236    safe_str(Name(mp->from), tbuf1, &bp);
237  else
238    safe_format(tbuf1, &bp, "%s (owner: %s)", Name(mp->from),
239                Name(Owner(mp->from)));
240  *bp = '\0';
241  return tbuf1;
242}
243
244/* Was this player the sender of this message? */
245static int
246was_sender(dbref player, MAIL *mp)
247{
248  /* If the dbrefs don't match, fail. */
249  if (mp->from != player)
250    return 0;
251  /* If we don't know the creation time of the sender, succeed. */
252  if (!mp->from_ctime)
253    return 1;
254  /* Succeed if and only if the creation times match. */
255  return (mp->from_ctime == CreTime(player));
256}
257
258/** Change folders or rename a folder.
259 * \verbatim
260 * This implements @mail/folder
261 * \endverbatim
262 * \param player the enactor.
263 * \param fld string containing folder number or name.
264 * \param newname string containing folder name, if renaming.
265 */
266void
267do_mail_change_folder(dbref player, char *fld, char *newname)
268{
269  int pfld;
270  char *p;
271
272  if (!fld || !*fld) {
273    /* Check mail in all folders */
274    for (pfld = MAX_FOLDERS; pfld >= 0; pfld--)
275      check_mail(player, pfld, 1);
276    pfld = player_folder(player);
277    notify_format(player,
278                  T("MAIL: Current folder is %d [%s]."), pfld,
279                  get_folder_name(player, pfld));
280    return;
281  }
282  pfld = parse_folder(player, fld);
283  if (pfld < 0) {
284    notify(player, T("MAIL: What folder is that?"));
285    return;
286  }
287  if (newname && *newname) {
288    /* We're changing a folder name here */
289    if (strlen(newname) > FOLDER_NAME_LEN) {
290      notify(player, T("MAIL: Folder name too long"));
291      return;
292    }
293    for (p = newname; p && *p; p++) {
294      if (!isalnum((unsigned char) *p)) {
295        notify(player, T("MAIL: Illegal folder name"));
296        return;
297      }
298    }
299    add_folder_name(player, pfld, newname);
300    notify_format(player, T("MAIL: Folder %d now named '%s'"), pfld, newname);
301  } else {
302    /* Set a new folder */
303    set_player_folder(player, pfld);
304    notify_format(player,
305                  T("MAIL: Current folder set to %d [%s]."), pfld,
306                  get_folder_name(player, pfld));
307  }
308}
309
310/** Remove a folder name.
311 * \verbatim
312 * This implements @mail/unfolder
313 * \endverbatim
314 * \param player the enactor.
315 * \param fld string containing folder number or name.
316 */
317void
318do_mail_unfolder(dbref player, char *fld)
319{
320  int pfld;
321
322  if (!fld || !*fld) {
323    notify(player, T("MAIL: You must specify a folder name or number"));
324    return;
325  }
326  pfld = parse_folder(player, fld);
327  if (pfld < 0) {
328    notify(player, T("MAIL: What folder is that?"));
329    return;
330  }
331  add_folder_name(player, pfld, NULL);
332  notify_format(player, T("MAIL: Folder %d now has no name"), pfld);
333}
334
335
336/** Tag a set of mail messages.
337 * \param player the enactor.
338 * \param msglist string specifying messages to tag.
339 */
340void
341do_mail_tag(dbref player, const char *msglist)
342{
343  do_mail_flags(player, msglist, M_TAG, 0);
344}
345
346/** Clear a set of mail messages.
347 * \param player the enactor.
348 * \param msglist string specifying messages to clear.
349 */
350void
351do_mail_clear(dbref player, const char *msglist)
352{
353  do_mail_flags(player, msglist, M_CLEARED, 0);
354}
355
356/** Untag a set of mail messages.
357 * \param player the enactor.
358 * \param msglist string specifying messages to untag.
359 */
360void
361do_mail_untag(dbref player, const char *msglist)
362{
363  do_mail_flags(player, msglist, M_TAG, 1);
364}
365
366/** Unclear a set of mail messages.
367 * \param player the enactor.
368 * \param msglist string specifying messages to unclear.
369 */
370void
371do_mail_unclear(dbref player, const char *msglist)
372{
373  do_mail_flags(player, msglist, M_CLEARED, 1);
374}
375
376
377/** Set or clear a flag on a set of messages.
378 * \param player the enactor.
379 * \param msglist string representing list of messages to operate on.
380 * \param flag flag to set or clear.
381 * \param negate if 1, clear the flag; if 0, set the flag.
382 */
383static void
384do_mail_flags(dbref player, const char *msglist, mail_flag flag, bool negate)
385{
386  MAIL *mp;
387  struct mail_selector ms;
388  int j;
389  mail_flag folder;
390  folder_array i;
391  int notified = 0;
392
393  if (!parse_msglist(msglist, &ms, player)) {
394    return;
395  }
396  FA_Init(i, j);
397  j = 0;
398  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
399  for (mp = find_exact_starting_point(player);
400       mp && (mp->to == player); mp = mp->next) {
401    if ((mp->to == player) && (All(ms) || (Folder(mp) == folder))) {
402      i[Folder(mp)]++;
403      if (mail_match(player, mp, ms, i[Folder(mp)])) {
404        j++;
405        if (negate) {
406          mp->read &= ~flag;
407        } else {
408          mp->read |= flag;
409        }
410        switch (flag) {
411        case M_TAG:
412          if (All(ms)) {
413            if (!notified) {
414              notify_format(player,
415                            T("MAIL: All messages in all folders %s."),
416                            negate ? "untagged" : "tagged");
417              notified++;
418            }
419          } else
420            notify_format(player,
421                          "MAIL: Msg #%d:%d %s.", Folder(mp),
422                          i[Folder(mp)], negate ? "untagged" : "tagged");
423          break;
424        case M_CLEARED:
425          if (All(ms)) {
426            if (!notified) {
427              notify_format(player,
428                            T("MAIL: All messages in all folders %s."),
429                            negate ? "uncleared" : "cleared");
430              notified++;
431            }
432          } else {
433            if (Unread(mp) && !negate) {
434              notify_format(player,
435                            T
436                            ("MAIL: Unread Msg #%d:%d cleared! Use @mail/unclear %d:%d to recover."),
437                            Folder(mp), i[Folder(mp)], Folder(mp),
438                            i[Folder(mp)]);
439            } else {
440              notify_format(player,
441                            (negate ? T("MAIL: Msg #%d:%d uncleared.") :
442                             T("MAIL: Msg #%d:%d cleared.")), Folder(mp),
443                            i[Folder(mp)]);
444            }
445          }
446          break;
447        }
448      }
449    }
450  }
451  if (!j) {
452    /* ran off the end of the list without finding anything */
453    notify(player, T("MAIL: You don't have any matching messages!"));
454  }
455  return;
456}
457
458/** File messages into a folder.
459 * \verbatim
460 * This implements @mail/file.
461 * \endverbatim
462 * \param player the enactor.
463 * \param msglist list of messages to file.
464 * \param folder name or number of folder to put messages in.
465 */
466void
467do_mail_file(dbref player, char *msglist, char *folder)
468{
469  MAIL *mp;
470  struct mail_selector ms;
471  int j, foldernum;
472  mail_flag origfold;
473  folder_array i;
474  int notified = 0;
475
476  if (!parse_msglist(msglist, &ms, player)) {
477    return;
478  }
479  if ((foldernum = parse_folder(player, folder)) == -1) {
480    notify(player, T("MAIL: Invalid folder specification"));
481    return;
482  }
483  FA_Init(i, j);
484  j = 0;
485  origfold = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
486  for (mp = find_exact_starting_point(player);
487       mp && (mp->to == player); mp = mp->next) {
488    if ((mp->to == player) && (All(ms) || (Folder(mp) == origfold))) {
489      i[Folder(mp)]++;
490      if (mail_match(player, mp, ms, i[Folder(mp)])) {
491        j++;
492        mp->read &= M_FMASK;    /* Clear the folder */
493        mp->read &= ~M_CLEARED; /* Unclear it if it was marked cleared */
494        mp->read |= FolderBit(foldernum);
495        if (All(ms)) {
496          if (!notified) {
497            notify_format(player,
498                          T("MAIL: All messages filed in folder %d [%s]"),
499                          foldernum, get_folder_name(player, foldernum));
500            notified++;
501          }
502        } else
503          notify_format(player,
504                        T("MAIL: Msg %d:%d filed in folder %d [%s]"),
505                        origfold, i[origfold], foldernum,
506                        get_folder_name(player, foldernum));
507      }
508    }
509  }
510  if (!j) {
511    /* ran off the end of the list without finding anything */
512    notify(player, T("MAIL: You don't have any matching messages!"));
513  }
514  return;
515}
516
517/** Read mail messages.
518 * This displays the contents of a set of mail messages.
519 * \param player the enactor.
520 * \param msglist list of messages to read.
521 */
522void
523do_mail_read(dbref player, char *msglist)
524{
525  MAIL *mp;
526  char tbuf1[BUFFER_LEN];
527  char folderheader[BUFFER_LEN];
528  struct mail_selector ms;
529  int j;
530  mail_flag folder;
531  folder_array i;
532
533  if (!parse_msglist(msglist, &ms, player)) {
534    return;
535  }
536  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
537  FA_Init(i, j);
538  j = 0;
539  for (mp = find_exact_starting_point(player);
540       mp && (mp->to == player); mp = mp->next) {
541    if ((mp->to == player) && (All(ms) || Folder(mp) == folder)) {
542      i[Folder(mp)]++;
543      if (mail_match(player, mp, ms, i[Folder(mp)])) {
544        /* Read it */
545        j++;
546        if (SUPPORT_PUEBLO) {
547          notify_noenter(player, open_tag("SAMP"));
548          snprintf(folderheader, BUFFER_LEN,
549                   "%c%cA XCH_HINT=\"List messages in this folder\" XCH_CMD=\"@mail/list %d:1-\"%c%s%c%c/A%c",
550                   TAG_START, MARKUP_HTML, Folder(mp), TAG_END, T("Folder:"),
551                   TAG_START, MARKUP_HTML, TAG_END);
552        } else
553          mush_strncpy(folderheader, T("Folder:"), BUFFER_LEN);
554        notify(player, DASH_LINE);
555        mush_strncpy(tbuf1, get_sender(mp, 1), BUFFER_LEN);
556        notify_format(player,
557                      T
558                      ("From: %-55s %s\nDate: %-25s   %s %2d   Message: %d\nStatus: %s"),
559                      tbuf1, ((*tbuf1 != '!') && IsPlayer(mp->from)
560                              && Connected(mp->from)
561                              && (!hidden(mp->from)
562                                  || Priv_Who(player))) ? " (Conn)" : "      ",
563                      show_time(mp->time, 0), folderheader, Folder(mp),
564                      i[Folder(mp)], status_string(mp));
565        notify_format(player, T("Subject: %s"), get_subject(mp));
566        notify(player, DASH_LINE);
567        if (SUPPORT_PUEBLO)
568          notify_noenter(player, close_tag("SAMP"));
569        strcpy(tbuf1, get_message(mp));
570        notify(player, tbuf1);
571        if (SUPPORT_PUEBLO)
572          notify(player, wrap_tag("SAMP", DASH_LINE));
573        else
574          notify(player, DASH_LINE);
575        if (Unread(mp))
576          mp->read |= M_MSGREAD;        /* mark message as read */
577      }
578    }
579  }
580  if (!j) {
581    /* ran off the end of the list without finding anything */
582    notify(player, T("MAIL: You don't have that many matching messages!"));
583  }
584  return;
585}
586
587
588/** List the flags, number, sender, subject, and date of messages in a
589 * concise format.
590 * \param player the enactor.
591 * \param msglist list of messages to list.
592 */
593void
594do_mail_list(dbref player, const char *msglist)
595{
596  char subj[30];
597  char sender[30];
598  MAIL *mp;
599  struct mail_selector ms;
600  int j;
601  mail_flag folder;
602  folder_array i;
603
604  if (!parse_msglist(msglist, &ms, player)) {
605    return;
606  }
607  FA_Init(i, j);
608  j = 0;
609  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
610  if (SUPPORT_PUEBLO)
611    notify_noenter(player, open_tag("SAMP"));
612  notify_format(player,
613                T
614                ("---------------------------  MAIL (folder %2d)  ------------------------------"),
615                folder);
616  for (mp = find_exact_starting_point(player); mp && (mp->to == player);
617       mp = mp->next) {
618    if ((mp->to == player) && (All(ms) || Folder(mp) == folder)) {
619      i[Folder(mp)]++;
620      if (mail_match(player, mp, ms, i[Folder(mp)])) {
621        /* list it */
622        if (SUPPORT_PUEBLO)
623          notify_noenter(player,
624                         tprintf
625                         ("%c%cA XCH_CMD=\"@mail/read %d:%d\" XCH_HINT=\"Read message %d in folder %d\"%c",
626                          TAG_START, MARKUP_HTML, Folder(mp), i[Folder(mp)],
627                          i[Folder(mp)], Folder(mp), TAG_END));
628        strcpy(subj, chopstr(get_subject(mp), 28));
629        strcpy(sender, chopstr(get_sender(mp, 0), 12));
630        notify_format(player, "[%s] %2d:%-3d %c%-12s  %-*s %s",
631                      status_chars(mp), Folder(mp), i[Folder(mp)],
632                      ((*sender != '!') && (Connected(mp->from) &&
633                                            (!hidden(mp->from)
634                                             || Priv_Who(player)))
635                       ? '*' : ' '), sender, 30, subj,
636                      mail_list_time(show_time(mp->time, 0), 1));
637        if (SUPPORT_PUEBLO)
638          notify_noenter(player, tprintf("%c%c/A%c", TAG_START,
639                                         MARKUP_HTML, TAG_END));
640      }
641    }
642  }
643  notify(player, DASH_LINE);
644  if (SUPPORT_PUEBLO)
645    notify(player, close_tag("SAMP"));
646  return;
647}
648
649static char *
650mail_list_time(const char *the_time, bool flag /* 1 for no year */ )
651{
652  static char newtime[BUFFER_LEN];
653  const char *p;
654  char *q;
655  int i;
656  p = the_time;
657  q = newtime;
658  if (!p || !*p)
659    return NULL;
660  /* Format of the_time is: day mon dd hh:mm:ss yyyy */
661  /* Chop out :ss */
662  for (i = 0; i < 16; i++) {
663    if (*p)
664      *q++ = *p++;
665  }
666  if (!flag) {
667    for (i = 0; i < 3; i++) {
668      if (*p)
669        p++;
670    }
671    for (i = 0; i < 5; i++) {
672      if (*p)
673        *q++ = *p++;
674    }
675  }
676  *q = '\0';
677  return newtime;
678}
679
680
681/** Expunge mail that's marked for deletion.
682 * \verbatim
683 * This implements @mail/purge.
684 * \endverbatim
685 * \param player the enactor.
686 */
687void
688do_mail_purge(dbref player)
689{
690  MAIL *mp, *nextp;
691
692  /* Go through player's mail, and remove anything marked cleared */
693  for (mp = find_exact_starting_point(player);
694       mp && (mp->to == player); mp = nextp) {
695    if ((mp->to == player) && Cleared(mp)) {
696      /* Delete this one */
697      /* head and tail of the list are special */
698      if (mp == HEAD)
699        HEAD = mp->next;
700      else if (mp == TAIL)
701        TAIL = mp->prev;
702      /* relink the list */
703      if (mp->prev != NULL)
704        mp->prev->next = mp->next;
705      if (mp->next != NULL)
706        mp->next->prev = mp->prev;
707      /* save the pointer */
708      nextp = mp->next;
709      /* then wipe */
710      mdb_top--;
711      free(mp->subject);
712      chunk_delete(mp->msgid);
713      slab_free(mail_slab, mp);
714    } else {
715      nextp = mp->next;
716    }
717  }
718  set_objdata(player, "MAIL", NULL);
719  if (command_check_byname(player, "@MAIL"))
720    notify(player, T("MAIL: Mailbox purged."));
721  return;
722}
723
724/** Forward mail messages to someone(s) else.
725 * \verbatim
726 * This implements @mail/forward.
727 * \endverbatim
728 * \param player the enactor.
729 * \param msglist list of messages to forward.
730 * \param tolist list of recipients to forwared to.
731 */
732void
733do_mail_fwd(dbref player, char *msglist, char *tolist)
734{
735  MAIL *mp;
736  MAIL *last;
737  struct mail_selector ms;
738  int j, num;
739  mail_flag folder;
740  folder_array i;
741  const char *head;
742  MAIL *temp;
743  dbref target;
744  int num_recpts = 0;
745  const char **start;
746  char *current;
747  start = &head;
748
749  if (!parse_msglist(msglist, &ms, player)) {
750    return;
751  }
752  if (!tolist || !*tolist) {
753    notify(player, T("MAIL: To whom should I forward?"));
754    return;
755  }
756  folder = AllInFolder(ms) ? player_folder(player) : MSFolder(ms);
757  /* Mark the player's last message. This prevents a loop if
758   * the forwarding command happens to forward a message back
759   * to the player itself
760   */
761  last = mp = find_exact_starting_point(player);
762  if (!last) {
763    notify(player, T("MAIL: You have no messages to forward."));
764    return;
765  }
766  while (last->next && (last->next->to == player))
767    last = last->next;
768
769  FA_Init(i, j);
770  while (mp && (mp->to == player) && (mp != last-&g