/** * \file malias.c * * \brief Global mail aliases/lists * * \verbatim * * This code implements an extension to extended @mail which allows * admin (and others who are so em@powered) to create mail aliases * for the MUSH. Optionally, any player can be allowed to. * * Aliases are used by @mail'ing to !<alias name> * Aliases have a name, a description, a list of members (dbrefs), an owner * a size (how many members), and two kinds of flags. * nflags control who can use/see an alias name, and mflags * control who can see the alias members. The choices * are everyone, alias members, owner, admin * * Interface: * @malias[/list] * @malias/members !name * @malias[/create] !name=list-of-members * @malias/destroy !name * @malias/add !name=list-of-members * @malias/remove !name=list-of-members * @malias/desc !name=description * @malias/nameprivs !name=flags * @malias/listprivs !name=flags * @malias/stat * @malias/chown !name=owner (Admin only) * @malias/nuke (Admin only) * * \endverbatim */ #define MA_INC 3 /**< How many maliases we malloc at a time */ #include "config.h" #include "copyrite.h" #ifdef I_SYS_TIME #include <sys/time.h> #else #include <time.h> #endif #include <ctype.h> #ifdef I_SYS_TYPES #include <sys/types.h> #endif #include <string.h> #include "conf.h" #include "externs.h" #include "mushdb.h" #include "dbdefs.h" #include "match.h" #include "parse.h" #include "malias.h" #include "privtab.h" #include "mymalloc.h" #include "flags.h" #include "pueblo.h" #include "log.h" #include "dbio.h" #include "confmagic.h" int ma_size = 0; /**< Number of maliases */ int ma_top = 0; /**< Top of alias array */ struct mail_alias *malias; /**< Pointer to linked list of aliases */ /** Privilege table for maliases. */ static PRIV malias_priv_table[] = { {"Admin", 'A', ALIAS_ADMIN, ALIAS_ADMIN}, {"Members", 'M', ALIAS_MEMBERS, ALIAS_MEMBERS}, {"Owner", 'O', ALIAS_OWNER, ALIAS_OWNER}, {NULL, '\0', 0, 0} }; static const char *get_shortprivs(struct mail_alias *m); /*********************************************************** ***** User-commands ***** ***********************************************************/ /** List or create a malias. * \verbatim * This implements the @malias command (with no switches). * \endverbatim * \param player the enactor. * \param arg1 name of malias to create or list, or NULL to list all. * \param arg2 parameters for creation, or NULL to list. */ void do_malias(dbref player, char *arg1, char *arg2) { if (!arg1 || !*arg1) { if (arg2 && *arg2) { notify(player, T("MAIL: Invalid malias command.")); return; } /* just the "@malias" command */ do_malias_list(player); return; } if (arg2 && *arg2) { /* Creating malias */ do_malias_create(player, arg1, arg2); } else { /* List specific alias - no arg2 */ do_malias_members(player, arg1); } } /** Create a malias. * \verbatim * This implements the @malias/create command. * \endverbatim * \param player the enactor. * \param alias name of malias to create. * \param tolist parameters for creation. */ void do_malias_create(dbref player, char *alias, char *tolist) { char *head, *tail, spot; struct mail_alias *m; char *na; const char *buff, *good, *scan; int i = 0; dbref target; dbref alist[100]; if (!IsPlayer(player)) { notify(player, T("MAIL: Only players may create mail aliases.")); return; } if (!alias || !*alias || !tolist || !*tolist) { notify(player, T("MAIL: What alias do you want to create?.")); return; } if (*alias != MALIAS_TOKEN) { notify_format(player, T("MAIL: All Mail aliases must begin with '%c'."), MALIAS_TOKEN); return; } good = "`$_-.'"; /* Make sure that the name contains legal characters only */ for (scan = alias + 1; scan && *scan; scan++) { if (isalpha((unsigned char) *scan) || isdigit((unsigned char) *scan)) continue; if (!strchr(good, *scan)) { notify(player, T("MAIL: Invalid character in mail alias.")); return; } } m = get_malias(GOD, alias); /* GOD can see all aliases */ if (m) { /* Ensures no duplicates! */ notify_format(player, T("MAIL: Mail Alias '%s' already exists."), alias); return; } if (!ma_size) { ma_size = MA_INC; malias = (struct mail_alias *) mush_malloc(sizeof(struct mail_alias) * ma_size, "malias_list"); } else if (ma_top >= ma_size) { ma_size += MA_INC; m = (struct mail_alias *) mush_malloc(sizeof(struct mail_alias) * (ma_size), "malias_list"); memcpy(m, malias, sizeof(struct mail_alias) * ma_top); mush_free((Malloc_t) malias, "malias_list"); malias = m; } i = 0; /* * Parse the player list */ head = (char *) tolist; while (head && *head) { while (*head == ' ') head++; tail = head; while (*tail && (*tail != ' ')) { if (*tail == '"') { head++; tail++; while (*tail && (*tail != '"')) tail++; } if (*tail) tail++; } tail--; if (*tail != '"') tail++; spot = *tail; *tail = '\0'; /* * Now locate a target */ if (!strcasecmp(head, "me")) target = player; else if (*head == '#') { target = atoi(head + 1); } else target = lookup_player(head); if (!(GoodObject(target)) || (!IsPlayer(target))) { notify_format(player, T("MAIL: No such player '%s'."), head); } else { buff = unparse_object(player, target); notify_format(player, T("MAIL: %s added to alias %s"), buff, alias); alist[i] = target; i++; } /* * Get the next recip */ *tail = spot; head = tail; if (*head == '"') head++; if (i == 100) break; } if (head && *head) { notify(player, T("MAIL: Alias list is restricted to maximal 100 entries!")); } if (!i) { notify(player, T("MAIL: No valid recipients for alias-list!")); return; } m = &malias[ma_top]; m->members = (dbref *) mush_malloc(sizeof(dbref) * i, "malias_members"); memcpy(m->members, alist, sizeof(dbref) * i); na = alias + 1; m->size = i; m->owner = player; m->name = mush_strdup(na, "malias_name"); m->desc = compress(na); add_check("malias_desc"); m->nflags = ALIAS_OWNER | ALIAS_MEMBERS; m->mflags = ALIAS_OWNER; ma_top++; notify_format(player, T("MAIL: Alias set '%s' defined."), alias); } /** List maliases. * \verbatim * This function implements @malias/list. * \endverbatim * \param player the enactor. */ void do_malias_list(dbref player) { struct mail_alias *m; int i = 0; int notified = 0; for (i = 0; i < ma_top; i++) { m = &malias[i]; if ((m->owner == player) || (m->nflags == 0) || ((m->nflags & ALIAS_ADMIN) && Hasprivs(player)) || ((m->nflags & ALIAS_MEMBERS) && ismember(m, player))) { if (!notified) { notify_format(player, "%-13s %-35s %s %-15s", T("Name"), T("Alias Description"), T("Use See"), T("Owner")); notified++; } notify_format(player, "%c%-12.12s %-35.35s %s %-15.15s", MALIAS_TOKEN, m->name, uncompress((unsigned char *) (m->desc)), get_shortprivs(m), Name(m->owner)); } } notify(player, T("***** End of Mail Aliases *****")); } /** List malias members. * \verbatim * This function implements @malias/members. * \endverbatim * \param player the enactor. * \param alias name of the alias to list members of. */ void do_malias_members(dbref player, char *alias) { struct mail_alias *m; int i = 0; char buff[BUFFER_LEN]; char *bp; m = get_malias(player, alias); if (!m) { notify_format(player, T("MAIL: Alias '%s' not found."), alias); return; } if ((m->owner == player) || (m->mflags == 0) || (Hasprivs(player)) || ((m->mflags & ALIAS_MEMBERS) && ismember(m, player))) { /* Dummy to avoid having to invert the "if" above ;-) */ } else { notify(player, T("MAIL: Permission denied.")); return; } bp = buff; safe_format(buff, &bp, T("MAIL: Alias %c%s: "), MALIAS_TOKEN, m->name); for (i = 0; i < m->size; i++) { safe_str(Name(m->members[i]), buff, &bp); safe_chr(' ', buff, &bp); /* Attention if player names may contain spaces!! */ } *bp = '\0'; notify(player, buff); } FUNCTION(fun_malias) { /* With no arguments, list all alias names * With one argument, it's either a delimiter or list all member dbrefs * With two arguments, it's a malias name and a delimiter, and we * list dbrefs, delimited */ int i; int count = 0; char sep = ' '; struct mail_alias *m; if (nargs >= 1) { m = get_malias(executor, args[0]); if (m) { if (!delim_check(buff, bp, nargs, args, 2, &sep)) return; if ((m->owner == executor) || (m->mflags == 0) || (Hasprivs(executor)) || ((m->mflags & ALIAS_MEMBERS) && ismember(m, executor))) { for (i = 0; i < m->size; i++) { if (count++) safe_chr(sep, buff, bp); safe_dbref(m->members[i], buff, bp); } } else { safe_str(T(e_perm), buff, bp); } return; } else { /* Perhaps it's a delimiter? */ if (arglens[0] > 1) { /* Oops, not if it's longer than one character */ safe_str(T(e_match), buff, bp); return; } if (!delim_check(buff, bp, nargs, args, 1, &sep)) return; } } /* List maliases, possibly with a delimiter */ for (i = 0; i < ma_top; i++) { m = &malias[i]; if ((m->owner == executor) || (m->nflags == 0) || ((m->nflags & ALIAS_ADMIN) && Hasprivs(executor)) || ((m->nflags & ALIAS_MEMBERS) && ismember(m, executor))) { if (count++) safe_chr(sep, buff, bp); safe_chr(MALIAS_TOKEN, buff, bp); safe_str(m->name, buff, bp); } } } /** Describe a malias. * \verbatim * This implements the @malias/desc command. * \endverbatim * \param player the enactor. * \param alias name of the malias to describe. * \param desc description to set. */ void do_malias_desc(dbref player, char *alias, char *desc) { struct mail_alias *m; if (!(m = get_malias(player, alias))) { notify_format(player, T("MAIL: Alias %s not found."), alias); return; } else if (Wizard(player) || (player == m->owner)) { if (m->desc) free(m->desc); /* No need to update MEM_CHECK records here */ m->desc = compress(desc); notify(player, T("MAIL: Description changed.")); } else notify(player, T("MAIL: Permission denied.")); return; } /** Change ownership of a malias. * \verbatim * This implements the @malias/chown command. * \endverbatim * \param player the enactor. * \param alias name of the malias to chown. * \param owner name of the new owner. */ void do_malias_chown(dbref player, char *alias, char *owner) { struct mail_alias *m; dbref no = NOTHING; if (!(m = get_malias(player, alias))) { notify_format(player, T("MAIL: Alias %s not found."), alias); return; } else { if (!Wizard(player)) { notify(player, T("MAIL: You cannot do that!")); return; } else { if ((no = lookup_player(owner)) == NOTHING) { notify(player, T("MAIL: I cannot find that player.")); return; } m->owner = no; notify(player, T("MAIL: Owner changed for alias.")); } } } /** Change name of a malias. * \verbatim * This implements the @malias/rename command. * \endverbatim * \param player the enactor. * \param alias name of the malias to rename. * \param newname new name for the malias. */ void do_malias_rename(dbref player, char *alias, char *newname) { struct mail_alias *m; if ((m = get_malias(player, alias)) == NULL) { notify(player, T("MAIL: I cannot find that alias!")); return; } if (*newname != MALIAS_TOKEN) { notify_format(player, T("MAIL: Bad alias. Aliases must start with '%c'."), MALIAS_TOKEN); return; } if (get_malias(GOD, newname) != NULL) { notify(player, T("MAIL: That name already exists!")); return; } if (!Wizard(player) && !(m->owner == player)) { notify(player, T("MAIL: Permission denied.")); return; } free(m->name); /* No need to update MEM_CHECK records here. */ m->name = strdup(newname + 1); notify(player, T("MAIL: Mail Alias renamed.")); } /** Delete a malias. * \verbatim * This implements the @malias/destroy command. * \endverbatim * \param player the enactor. * \param alias name of the malias to destroy. */ void do_malias_destroy(dbref player, char *alias) { struct mail_alias *m; m = get_malias(player, alias); if (!m) { notify(player, T ("MAIL: Not a valid alias. Remember to prefix the alias name with *.")); return; } if (Wizard(player) || (m->owner == player)) { notify(player, T("MAIL: Alias Destroyed.")); if (m->members) mush_free((Malloc_t) m->members, "malias_members"); if (m->name) mush_free(m->name, "malias_name"); if (m->desc) mush_free(m->desc, "malias_desc"); *m = malias[--ma_top]; } else { notify(player, T("MAIL: Permission denied!")); } } /** Set the membership list for a malias. * \verbatim * This implements the @malias/set command. * \endverbatim * \param player the enactor. * \param alias name of the malias to set members for. * \param tolist space-separated list of players to set as members. */ void do_malias_set(dbref player, char *alias, char *tolist) { struct mail_alias *m; int i = 0; char *head, *tail, spot; const char *buff; dbref alist[100]; dbref target; m = get_malias(player, alias); if (!m) { notify_format(player, T ("MAIL: Not a valid alias. Remember to prefix the alias name with %c."), MALIAS_TOKEN); return; } if (!tolist || !*tolist) { notify(player, T("MAIL: You must set the alias to a non-empty list.")); return; } if (!(Wizard(player) || (m->owner == player))) { notify(player, T("MAIL: Permission denied!")); return; } /* * Parse the player list */ head = (char *) tolist; while (head && *head) { while (*head == ' ') head++; tail = head; while (*tail && (*tail != ' ')) { if (*tail == '"') { head++; tail++; while (*tail && (*tail != '"')) tail++; } if (*tail) tail++; } tail--; if (*tail != '"') tail++; spot = *tail; *tail = '\0'; /* * Now locate a target */ if (!strcasecmp(head, "me")) target = player; else if (*head == '#') { target = atoi(head + 1); } else target = lookup_player(head); if (!(GoodObject(target)) || (!IsPlayer(target))) { notify_format(player, T("MAIL: No such player '%s'."), head); } else { buff = unparse_object(player, target); notify_format(player, T("MAIL: %s added to alias %s"), buff, alias); alist[i] = target; i++; } /* * Get the next recip */ *tail = spot; head = tail; if (*head == '"') head++; if (i == 100) break; } if (head && *head) { notify(player, T("MAIL: Alias list is restricted to maximal 100 entries!")); } if (!i) { notify(player, T("MAIL: No valid recipients for alias-list!")); return; } if (m->members) mush_free((Malloc_t) m->members, "malias_members"); m->members = (dbref *) mush_malloc(sizeof(dbref) * i, "malias_members"); memcpy(m->members, alist, sizeof(dbref) * i); m->size = i; notify(player, T("MAIL: Alias list set.")); } /** List all maliases. * \verbatim * This implements the @malias/list command. * \endverbatim * \param player the enactor. */ void do_malias_all(dbref player) { struct mail_alias *m; int i; if (!Hasprivs(player)) { do_malias_list(player); return; } notify(player, "Num Name Description Owner Count"); for (i = 0; i < ma_top; i++) { m = &malias[i]; notify_format(player, "#%-4d %c%-10.10s %-40.40s %-11.11s (%3d)", i, MALIAS_TOKEN, m->name, uncompress((unsigned char *) m->desc), Name(m->owner), m->size); } notify(player, T("***** End of Mail Aliases *****")); } /** Statistics on maliases. * \verbatim * This implements the @malias/stat command. * \endverbatim * \param player the enactor. */ void do_malias_stats(dbref player) { if (!Hasprivs(player)) notify(player, T("MAIL: Permission denied.")); else { notify_format(player, T("MAIL: Number of mail aliases defined: %d"), ma_top); notify_format(player, T("MAIL: Allocated slots %d"), ma_size); } } /** Remove all maliases. * \verbatim * This implements the @malias/nuke command. * \endverbatim * \param player the enactor. */ void do_malias_nuke(dbref player) { struct mail_alias *m; int i; if (!God(player)) { notify(player, T("MAIL: Only god can do that!")); return; } if (ma_size) { /* aliases defined ? */ for (i = 0; i < ma_top; i++) { m = &malias[i]; if (m->name) mush_free(m->name, "malias_name"); if (m->desc) mush_free(m->desc, "malias_desc"); if (m->members) mush_free((Malloc_t) m->members, "malias_members"); } mush_free((Malloc_t) malias, "malias_list"); } ma_size = ma_top = 0; notify(player, T("MAIL: All mail aliases destroyed!")); } /** Set permisions on maliases. * \verbatim * This implements @malias/use and @malias/see * \endverbatim * \param player the enactor. * \param alias name of the malias. * \param privs string of privs to set. * \param type if 1, setting nprivs, if 0, mprivs. */ void do_malias_privs(dbref player, char *alias, char *privs, int type) { struct mail_alias *m; int *p; if (!(m = get_malias(player, alias))) { notify(player, T("MAIL: I cannot find that alias!")); return; } if (!Wizard(player) && (m->owner != player)) { notify(player, T("MAIL: Permission denied.")); return; } p = type ? &m->mflags : &m->nflags; *p = string_to_privs(malias_priv_table, privs, 0); notify_format(player, T("MAIL: Permission to see/use alias '%s' changed to %s"), alias, privs_to_string(malias_priv_table, *p)); } /** Add players to a malias. * \param player dbref of enactor. * \param alias name of malias. * \param tolist string with list of players to add. */ void do_malias_add(dbref player, char *alias, char *tolist) { char *head, *tail, spot; struct mail_alias *m; const char *buff; int i = 0; dbref target; dbref alist[100]; dbref *members; m = get_malias(player, alias); if (!m) { notify_format(player, T("MAIL: Mail Alias '%s' not found."), alias); return; } if (!Wizard(player) && (m->owner != player)) { notify(player, T("Permission denied.")); return; } i = 0; /* * Parse the player list */ head = (char *) tolist; while (head && *head) { while (*head == ' ') head++; tail = head; while (*tail && (*tail != ' ')) { if (*tail == '"') { head++; tail++; while (*tail && (*tail != '"')) tail++; } if (*tail) tail++; } tail--; if (*tail != '"') tail++; spot = *tail; *tail = '\0'; /* * Now locate a target */ if (!strcasecmp(head, "me")) target = player; else if (*head == '#') { target = atoi(head + 1); } else target = lookup_player(head); if (!(GoodObject(target)) || (!IsPlayer(target))) { notify_format(player, T("MAIL: No such player '%s'."), head); } else { if (ismember(m, target)) { notify_format(player, T("MAIL: player '%s' exists already in alias %s."), head, alias); } else { buff = unparse_object(player, target); notify_format(player, T("MAIL: %s added to alias %s"), buff, alias); alist[i] = target; i++; } } /* * Get the next recip */ *tail = spot; head = tail; if (*head == '"') head++; if (i == 100) break; } if (head && *head) { notify(player, T("MAIL: Alias list is restricted to maximal 100 entries!")); } if (!i) { notify(player, T("MAIL: No valid recipients for alias-list!")); return; } members = (dbref *) mush_malloc(sizeof(dbref) * (i + m->size), "malias_members"); memcpy(members, m->members, sizeof(dbref) * m->size); memcpy(&members[m->size], alist, sizeof(dbref) * i); mush_free((Malloc_t) m->members, "malias_members"); m->members = members; m->size += i; notify_format(player, T("MAIL: Alias set '%s' redefined."), alias); } /** Remove players from a malias. * \param player dbref of enactor. * \param alias name of malias. * \param tolist string with list of players to remove. */ void do_malias_remove(dbref player, char *alias, char *tolist) { char *head, *tail, spot; struct mail_alias *m; const char *buff; int i = 0; dbref target; m = get_malias(player, alias); if (!m) { notify_format(player, T("MAIL: Mail Alias '%s' not found."), alias); return; } if (!Wizard(player) && (m->owner != player)) { notify(player, T("Permission denied.")); return; } i = 0; /* * Parse the player list */ head = (char *) tolist; while (head && *head) { while (*head == ' ') head++; tail = head; while (*tail && (*tail != ' ')) { if (*tail == '"') { head++; tail++; while (*tail && (*tail != '"')) tail++; } if (*tail) tail++; } tail--; if (*tail != '"') tail++; spot = *tail; *tail = '\0'; /* * Now locate a target */ if (!strcasecmp(head, "me")) target = player; else if (*head == '#') { target = atoi(head + 1); } else target = lookup_player(head); if (!(GoodObject(target)) || (!IsPlayer(target))) { notify_format(player, T("MAIL: No such player '%s'."), head); } else { if (!(i = ismember(m, target))) { notify_format(player, T("MAIL: player '%s' is not in alias %s."), head, alias); } else { buff = unparse_object(player, target); m->members[i - 1] = m->members[--m->size]; notify_format(player, T("MAIL: %s removed from alias %s"), buff, alias); } } /* * Get the next recip */ *tail = spot; head = tail; if (*head == '"') head++; } notify_format(player, T("MAIL: Alias set '%s' redefined."), alias); } /*********************************************************** ***** "Utility" functions ***** ***********************************************************/ static const char * get_shortprivs(struct mail_alias *m) { static char privs[10]; strcpy(privs, "-- -- "); if (!m->nflags) privs[0] = 'E'; else { if (m->nflags & ALIAS_MEMBERS) privs[0] = 'M'; if (m->nflags & ALIAS_ADMIN) privs[1] = 'A'; if (!strncmp(privs, "--", 2)) privs[1] = 'O'; } if (!m->mflags) privs[4] = 'E'; else { if (m->mflags & ALIAS_MEMBERS) privs[4] = 'M'; if (m->mflags & ALIAS_ADMIN) privs[5] = 'A'; if (!strncmp(privs + 4, "--", 2)) privs[5] = 'O'; } return privs; } /** Is a player a member of a malias? * \param m pointer to malias. * \param player dbref of player. * \retval 1 player is a member of the malias. * \retval 0 player is not a member of the malias. */ int ismember(struct mail_alias *m, dbref player) { int i; for (i = 0; i < m->size; i++) { if (player == m->members[i]) return (i + 1); /* To avoid entry "0" */ } return 0; } /** Remove a destroyed player from all maliases. * \param player player to remove from maliases. */ void malias_cleanup(dbref player) { struct mail_alias *m; int n, i = 0; for (n = 0; n < ma_top; n++) { m = &malias[n]; if ((i = ismember(m, player)) != 0) { do_rawlog(LT_ERR, "Removing #%d from malias %s", player, m->name); m->members[i - 1] = m->members[--m->size]; } } } /** Get a malias pointer with permission checking. * \param player player dbref, for permission check. * \param alias name of malias to retrieve. * \return pointer to malias structure, or NULL if player can't see it. */ struct mail_alias * get_malias(dbref player, char *alias) { const char *mal; struct mail_alias *m; int i = 0; if (*alias != MALIAS_TOKEN) return NULL; mal = alias + 1; for (i = 0; i < ma_top; i++) { m = &malias[i]; if ((m->owner == player) || (m->nflags == 0) || /* ((m->nflags & ALIAS_ADMIN) && Hasprivs(player)) || */ Hasprivs(player) || ((m->nflags & ALIAS_MEMBERS) && ismember(m, player))) { if (!strcasecmp(mal, m->name)) return m; } } return NULL; } /*********************************************************** ***** Loading and saving of mail-aliases ***** ***********************************************************/ /** Load maliases from the mail db. * \param fp file pointer to read from. */ void load_malias(FILE * fp) { int i, j; char buffer[BUFFER_LEN]; struct mail_alias *m; char *s; ma_top = getref(fp); ma_size = ma_top; if (ma_top > 0) malias = (struct mail_alias *) mush_malloc(sizeof(struct mail_alias) * ma_size, "malias_list"); else malias = NULL; for (i = 0; i < ma_top; i++) { m = &malias[i]; m->owner = getref(fp); m->name = mush_strdup(getstring_noalloc(fp), "malias_name"); m->desc = compress(getstring_noalloc(fp)); add_check("malias_desc"); m->nflags = getref(fp); m->mflags = getref(fp); m->size = getref(fp); if (m->size > 0) { m->members = (dbref *) mush_malloc(m->size * sizeof(dbref), "malias_members"); for (j = 0; j < m->size; j++) { m->members[j] = getref(fp); } } else { m->members = NULL; } } s = fgets(buffer, sizeof(buffer), fp); if (!s || strcmp(buffer, "\"*** End of MALIAS ***\"\n") != 0) { do_rawlog(LT_ERR, T("MAIL: Error reading MALIAS list")); } } /** Write maliases to the maildb * \param fp file pointer to write to. */ void save_malias(FILE * fp) { int i, j; struct mail_alias *m; putref(fp, ma_top); for (i = 0; i < ma_top; i++) { m = &malias[i]; putref(fp, m->owner); putstring(fp, (char *) (m->name)); putstring(fp, uncompress(m->desc)); putref(fp, m->nflags); putref(fp, m->mflags); putref(fp, m->size); for (j = 0; j < m->size; j++) putref(fp, m->members[j]); } putstring(fp, "*** End of MALIAS ***"); }