1 // Message-related protocol commands for Citadel clients
3 // Copyright (c) 1987-2024 by the citadel.org team
5 // This program is open source software. Use, duplication, or disclosure
6 // is subject to the terms of the GNU General Public License version 3.
9 #include <libcitadel.h>
10 #include "../../citserver.h"
11 #include "../../ctdl_module.h"
12 #include "../../internet_addressing.h"
13 #include "../../user_ops.h"
14 #include "../../room_ops.h"
15 #include "../../config.h"
17 extern char *msgkeys[];
20 // Back end for the MSGS command: output message number only.
21 void simple_listing(long msgnum, void *userdata) {
22 cprintf("%ld\n", msgnum);
26 // Back end for the MSGS command: output header summary.
27 void headers_listing(long msgnum, void *userdata) {
28 struct CtdlMessage *msg;
29 int output_mode = *(int *)userdata;
31 msg = CtdlFetchMessage(msgnum, 0);
33 cprintf("%ld|0|||||||\n", msgnum);
37 // change all vertical bars in the subject to hyphens so it doesn't screw up the protocol
38 if (!CM_IsEmpty(msg, eMsgSubject)) {
40 for (p=msg->cm_fields[eMsgSubject]; *p; p++) {
47 // output all fields except the references hash
48 cprintf("%ld|%s|%s|%s|%s|%s",
50 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"),
51 (!CM_IsEmpty(msg, eAuthor) ? msg->cm_fields[eAuthor] : ""),
52 CtdlGetConfigStr("c_nodename"), // no more nodenames anymore
53 (!CM_IsEmpty(msg, erFc822Addr) ? msg->cm_fields[erFc822Addr] : ""),
54 (!CM_IsEmpty(msg, eMsgSubject) ? msg->cm_fields[eMsgSubject] : "")
57 if (output_mode == MSG_HDRS_THREADS) { // field view with thread hashes
59 // output the references hash
61 (!CM_IsEmpty(msg, emessageId) ? HashLittle(msg->cm_fields[emessageId],strlen(msg->cm_fields[emessageId])) : 0)
64 // output the references hash (yes it's ok that we're trashing the source buffer by doing this)
65 if (!CM_IsEmpty(msg, eWeferences)) {
67 char *rest = msg->cm_fields[eWeferences];
69 while((token = strtok_r(rest, "|", &rest))) {
70 cprintf("%d%s", HashLittle(token,rest-prev-(*rest==0?0:1)), (*rest==0?"":","));
78 else { // field view with no threads, subject extends out forever
85 typedef struct _msg_filter {
92 void headers_brief_filter(long msgnum, void *userdata) {
94 struct CtdlMessage *msg;
95 msg_filter *flt = (msg_filter*) userdata;
97 l = GetCount(flt->Filter);
98 msg = CtdlFetchMessage(msgnum, 0);
99 StrBufPrintf(flt->buffer, "%ld", msgnum);
101 for (i = 0; i < l; i++) {
102 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
109 RewindHashPos(flt->Filter, flt->p, 0);
110 while (GetNextHashPos(flt->Filter, flt->p, &len, &k, &v)) {
111 eMsgField f = (eMsgField) v;
113 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
114 if (!CM_IsEmpty(msg, f)) {
115 StrBufAppendBufPlain(flt->buffer, CM_KEY(msg, f), 0);
119 StrBufAppendBufPlain(flt->buffer, HKEY("\n"), 0);
120 cputbuf(flt->buffer);
123 // Back end for the MSGS command: output EUID header.
124 void headers_euid(long msgnum, void *userdata) {
125 struct CtdlMessage *msg;
127 msg = CtdlFetchMessage(msgnum, 0);
129 cprintf("%ld||\n", msgnum);
133 cprintf("%ld|%s|%s\n",
135 (!CM_IsEmpty(msg, eExclusiveID) ? msg->cm_fields[eExclusiveID] : ""),
136 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"));
141 // cmd_msgs() - get list of message #'s in this room
142 // implements the MSGS server command using CtdlForEachMessage()
143 void cmd_msgs(char *cmdbuf) {
150 int with_template = 0;
151 struct CtdlMessage *template = NULL;
153 char search_string[1024];
154 ForEachMsgCallback CallBack;
156 if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
158 extract_token(which, cmdbuf, 0, '|', sizeof which);
159 cm_ref = extract_int(cmdbuf, 1);
160 extract_token(search_string, cmdbuf, 1, '|', sizeof search_string);
161 with_template = extract_int(cmdbuf, 2);
162 int output_mode = extract_int(cmdbuf, 3);
163 switch (output_mode) {
166 CallBack = simple_listing;
169 case MSG_HDRS_THREADS:
170 CallBack = headers_listing;
173 CallBack = headers_euid;
175 case MSG_HDRS_BRIEFFILTER:
177 CallBack = headers_brief_filter;
182 if (!strncasecmp(which, "OLD", 3))
184 else if (!strncasecmp(which, "NEW", 3))
186 else if (!strncasecmp(which, "FIRST", 5))
188 else if (!strncasecmp(which, "LAST", 4))
190 else if (!strncasecmp(which, "GT", 2))
192 else if (!strncasecmp(which, "LT", 2))
194 else if (!strncasecmp(which, "SEARCH", 6))
199 if ( (mode == MSGS_SEARCH) && (!CtdlGetConfigInt("c_enable_fulltext")) ) {
200 cprintf("%d Full text index is not enabled on this server.\n",
201 ERROR + CMD_NOT_SUPPORTED);
205 if (with_template == 1) {
208 cprintf("%d Send template then receive message list\n", SEND_THEN_RECV);
209 template = (struct CtdlMessage *) malloc(sizeof(struct CtdlMessage));
210 memset(template, 0, sizeof(struct CtdlMessage));
211 template->cm_magic = CTDLMESSAGE_MAGIC;
212 template->cm_anon_type = MES_NORMAL;
214 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
218 tValueLen = extract_token(tfield, buf, 0, '|', sizeof tfield);
219 if ((tValueLen == 4) && GetFieldFromMnemonic(&f, tfield)) {
220 tValueLen = extract_token(tvalue, buf, 1, '|', sizeof tvalue);
221 if (tValueLen >= 0) {
222 CM_SetField(template, f, tvalue);
228 else if (with_template == 2) {
231 cprintf("%d Send list of headers\n", SEND_THEN_RECV);
232 filt.Filter = NewHash(1, lFlathash);
233 filt.buffer = NewStrBufPlain(NULL, 1024);
234 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
237 if (GetFieldFromMnemonic(&f, buf)) {
238 Put(filt.Filter, LKEY(i), (void*)f, reference_free_handler);
242 filt.p = GetNewHashPos(filt.Filter, 0);
246 cprintf("%d \n", LISTING_FOLLOWS);
249 if (with_template < 2) {
250 CtdlForEachMessage(mode,
251 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
252 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
258 if (template != NULL) CM_Free(template);
261 CtdlForEachMessage(mode,
262 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
263 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
269 DeleteHashPos(&filt.p);
270 DeleteHash(&filt.Filter);
271 FreeStrBuf(&filt.buffer);
278 // display a message (mode 0 - Citadel proprietary)
279 void cmd_msg0(char *cmdbuf) {
281 int headers_only = HEADERS_ALL;
283 msgid = extract_long(cmdbuf, 0);
284 headers_only = extract_int(cmdbuf, 1);
286 CtdlOutputMsg(msgid, MT_CITADEL, headers_only, 1, 0, NULL, 0, NULL, NULL, NULL);
291 // display a message (mode 2 - RFC822)
292 void cmd_msg2(char *cmdbuf) {
294 int headers_only = HEADERS_ALL;
296 msgid = extract_long(cmdbuf, 0);
297 headers_only = extract_int(cmdbuf, 1);
299 CtdlOutputMsg(msgid, MT_RFC822, headers_only, 1, 1, NULL, 0, NULL, NULL, NULL);
303 // Display a message using MIME content types
304 void cmd_msg4(char *cmdbuf) {
308 msgid = extract_long(cmdbuf, 0);
309 extract_token(section, cmdbuf, 1, '|', sizeof section);
310 CtdlOutputMsg(msgid, MT_MIME, 0, 1, 0, (section[0] ? section : NULL) , 0, NULL, NULL, NULL);
314 // Client tells us its preferred message format(s)
315 void cmd_msgp(char *cmdbuf) {
316 if (!strcasecmp(cmdbuf, "dont_decode")) {
317 CC->msg4_dont_decode = 1;
318 cprintf("%d MSG4 will not pre-decode messages.\n", CIT_OK);
321 safestrncpy(CC->preferred_formats, cmdbuf, sizeof(CC->preferred_formats));
322 cprintf("%d Preferred MIME formats have been set.\n", CIT_OK);
327 // Open a component of a MIME message as a download file
328 void cmd_opna(char *cmdbuf) {
330 char desired_section[128];
332 msgid = extract_long(cmdbuf, 0);
333 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
334 safestrncpy(CC->download_desired_section, desired_section, sizeof CC->download_desired_section);
335 CtdlOutputMsg(msgid, MT_DOWNLOAD, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
339 // Open a component of a MIME message and transmit it all at once
340 void cmd_dlat(char *cmdbuf) {
342 char desired_section[128];
344 msgid = extract_long(cmdbuf, 0);
345 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
346 safestrncpy(CC->download_desired_section, desired_section, sizeof CC->download_desired_section);
347 CtdlOutputMsg(msgid, MT_SPEW_SECTION, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
351 // message entry - mode 0 (normal)
352 void cmd_ent0(char *entargs) {
357 char supplied_euid[128];
360 char newusername[256];
361 char newuseremail[256];
362 struct CtdlMessage *msg;
366 struct recptypes *valid = NULL;
367 struct recptypes *valid_to = NULL;
368 struct recptypes *valid_cc = NULL;
369 struct recptypes *valid_bcc = NULL;
371 int subject_required = 0;
376 int newuseremail_ok = 0;
377 char references[SIZ];
382 post = extract_int(entargs, 0);
383 extract_token(recp, entargs, 1, '|', sizeof recp);
384 anon_flag = extract_int(entargs, 2);
385 format_type = extract_int(entargs, 3);
386 extract_token(subject, entargs, 4, '|', sizeof subject);
387 extract_token(newusername, entargs, 5, '|', sizeof newusername);
388 do_confirm = extract_int(entargs, 6);
389 extract_token(cc, entargs, 7, '|', sizeof cc);
390 extract_token(bcc, entargs, 8, '|', sizeof bcc);
391 switch(CC->room.QRdefaultview) {
394 extract_token(supplied_euid, entargs, 9, '|', sizeof supplied_euid);
397 supplied_euid[0] = 0;
400 extract_token(newuseremail, entargs, 10, '|', sizeof newuseremail);
401 extract_token(references, entargs, 11, '|', sizeof references);
402 for (ptr=references; *ptr != 0; ++ptr) {
403 if (*ptr == '!') *ptr = '|';
406 // first check to make sure the request is valid.
408 err = CtdlDoIHavePermissionToPostInThisRoom(
412 (!IsEmptyStr(references)) // is this a reply? or a top-level post?
415 cprintf("%d %s\n", err, errmsg);
419 // Check some other permission type things.
421 if (IsEmptyStr(newusername)) {
422 strcpy(newusername, CC->user.fullname);
424 if ( (CC->user.axlevel < AxAideU)
425 && (strcasecmp(newusername, CC->user.fullname))
426 && (strcasecmp(newusername, CC->cs_inet_fn))
428 cprintf("%d You don't have permission to author messages as '%s'.\n",
429 ERROR + HIGHER_ACCESS_REQUIRED,
435 if (IsEmptyStr(newuseremail)) {
439 if (!IsEmptyStr(newuseremail)) {
440 if (!strcasecmp(newuseremail, CC->cs_inet_email)) {
443 else if (!IsEmptyStr(CC->cs_inet_other_emails)) {
444 j = num_tokens(CC->cs_inet_other_emails, '|');
445 for (i=0; i<j; ++i) {
446 extract_token(buf, CC->cs_inet_other_emails, i, '|', sizeof buf);
447 if (!strcasecmp(newuseremail, buf)) {
454 if (!newuseremail_ok) {
455 cprintf("%d You don't have permission to author messages as '%s'.\n",
456 ERROR + HIGHER_ACCESS_REQUIRED,
462 CC->cs_flags |= CS_POSTING;
464 // In mailbox rooms we have to behave a little differently --
465 // make sure the user has specified at least one recipient. Then
466 // validate the recipient(s). We do this for the Mail> room, as
467 // well as any room which has the "Mailbox" view set - unless it
468 // is the DRAFTS room which does not require recipients.
470 if ( ( ( (CC->room.QRflags & QR_MAILBOX) && (!strcasecmp(&CC->room.QRname[11], MAILROOM)) )
471 || ( (CC->room.QRflags & QR_MAILBOX) && (CC->curr_view == VIEW_MAILBOX) )
472 ) && (strcasecmp(&CC->room.QRname[11], USERDRAFTROOM)) !=0 ) {
473 if (CC->user.axlevel < AxProbU) {
474 strcpy(recp, "sysop");
479 valid_to = validate_recipients(recp, 0);
480 if (valid_to->num_error > 0) {
481 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_to->errormsg);
482 free_recipients(valid_to);
486 valid_cc = validate_recipients(cc, 0);
487 if (valid_cc->num_error > 0) {
488 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_cc->errormsg);
489 free_recipients(valid_to);
490 free_recipients(valid_cc);
494 valid_bcc = validate_recipients(bcc, 0);
495 if (valid_bcc->num_error > 0) {
496 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_bcc->errormsg);
497 free_recipients(valid_to);
498 free_recipients(valid_cc);
499 free_recipients(valid_bcc);
503 // Recipient required, but none were specified
504 if ( (valid_to->num_error < 0) && (valid_cc->num_error < 0) && (valid_bcc->num_error < 0) ) {
505 free_recipients(valid_to);
506 free_recipients(valid_cc);
507 free_recipients(valid_bcc);
508 cprintf("%d At least one recipient is required.\n", ERROR + NO_SUCH_USER);
512 if (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0) {
513 if (CtdlCheckInternetMailPermission(&CC->user)==0) {
514 cprintf("%d You do not have permission to send Internet mail.\n",
515 ERROR + HIGHER_ACCESS_REQUIRED);
516 free_recipients(valid_to);
517 free_recipients(valid_cc);
518 free_recipients(valid_bcc);
523 if ( ( (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet) > 0) && (CC->user.axlevel < AxNetU) ) {
524 cprintf("%d Higher access required for network mail.\n", ERROR + HIGHER_ACCESS_REQUIRED);
525 free_recipients(valid_to);
526 free_recipients(valid_cc);
527 free_recipients(valid_bcc);
531 if ( (RESTRICT_INTERNET == 1)
532 && (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0)
533 && ((CC->user.flags & US_INTERNET) == 0)
534 && (!CC->internal_pgm)
536 cprintf("%d You don't have access to Internet mail.\n", ERROR + HIGHER_ACCESS_REQUIRED);
537 free_recipients(valid_to);
538 free_recipients(valid_cc);
539 free_recipients(valid_bcc);
545 // Is this a room which has anonymous-only or anonymous-option?
546 anonymous = MES_NORMAL;
547 if (CC->room.QRflags & QR_ANONONLY) {
548 anonymous = MES_ANONONLY;
550 if (CC->room.QRflags & QR_ANONOPT) {
551 if (anon_flag == 1) { // only if the user requested it
552 anonymous = MES_ANONOPT;
556 if ((CC->room.QRflags & QR_MAILBOX) == 0) {
560 // Recommend to the client that the use of a message subject is
561 // strongly recommended in this room, if either the SUBJECTREQ flag
562 // is set, or if there is one or more Internet email recipients.
564 if (CC->room.QRflags2 & QR2_SUBJECTREQ) subject_required = 1;
565 if ((valid_to) && (valid_to->num_internet > 0)) subject_required = 1;
566 if ((valid_cc) && (valid_cc->num_internet > 0)) subject_required = 1;
567 if ((valid_bcc) && (valid_bcc->num_internet > 0)) subject_required = 1;
569 // If we're only checking the validity of the request, return success without creating the message.
571 cprintf("%d %s|%d\n", CIT_OK,
572 ((valid_to != NULL) ? valid_to->display_recp : ""),
574 free_recipients(valid_to);
575 free_recipients(valid_cc);
576 free_recipients(valid_bcc);
580 // We don't need these anymore because we'll do it differently below
581 free_recipients(valid_to);
582 free_recipients(valid_cc);
583 free_recipients(valid_bcc);
585 // Read in the message from the client.
587 cprintf("%d send message\n", SEND_THEN_RECV);
590 cprintf("%d send message\n", SEND_LISTING);
593 msg = CtdlMakeMessage(
595 CC->room.QRname, anonymous, format_type,
596 newusername, newuseremail, subject,
597 ((!IsEmptyStr(supplied_euid)) ? supplied_euid : NULL),
601 // Put together one big recipients struct containing to/cc/bcc all in one. This is for the envelope.
602 char *all_recps = malloc(SIZ * 3);
603 strcpy(all_recps, recp);
604 if (!IsEmptyStr(cc)) {
605 if (!IsEmptyStr(all_recps)) {
606 strcat(all_recps, ",");
608 strcat(all_recps, cc);
610 if (!IsEmptyStr(bcc)) {
611 if (!IsEmptyStr(all_recps)) {
612 strcat(all_recps, ",");
614 strcat(all_recps, bcc);
616 if (!IsEmptyStr(all_recps)) {
617 valid = validate_recipients(all_recps, 0);
624 // posting into a mailing list room? set the envelope from
625 // to the actual mail address so others get a valid reply-to-header.
626 if ((valid != NULL) && (valid->num_room == 1) && !IsEmptyStr(valid->recp_orgroom)) {
627 CM_SetField(msg, eenVelopeTo, valid->recp_orgroom);
631 msgnum = CtdlSubmitMsg(msg, valid, "");
633 cprintf("%ld\n", msgnum);
635 if (StrLength(CC->StatusMessage) > 0) {
636 cprintf("%s\n", ChrPtr(CC->StatusMessage));
638 else if (msgnum >= 0L) {
639 client_write(HKEY("Message accepted.\n"));
642 client_write(HKEY("Internal error.\n"));
645 if (!CM_IsEmpty(msg, eExclusiveID)) {
646 cprintf("%s\n", msg->cm_fields[eExclusiveID]);
657 free_recipients(valid);
663 // Delete message from current room
664 void cmd_dele(char *args) {
672 extract_token(msgset, args, 0, '|', sizeof msgset);
673 num_msgs = num_tokens(msgset, ',');
675 cprintf("%d Nothing to do.\n", CIT_OK);
679 if (CtdlDoIHavePermissionToDeleteMessagesFromThisRoom() == 0) {
680 cprintf("%d Higher access required.\n", ERROR + HIGHER_ACCESS_REQUIRED);
684 // Build our message set to be moved/copied
685 msgs = malloc(num_msgs * sizeof(long));
686 for (i=0; i<num_msgs; ++i) {
687 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
688 msgs[i] = atol(msgtok);
691 num_deleted = CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
695 cprintf("%d %d message%s deleted.\n", CIT_OK, num_deleted, ((num_deleted != 1) ? "s" : ""));
698 cprintf("%d Message not found.\n", ERROR + MESSAGE_NOT_FOUND);
703 // move or copy a message to another room
704 void cmd_move(char *args) {
710 char targ[ROOMNAMELEN];
711 struct ctdlroom qtemp;
718 extract_token(msgset, args, 0, '|', sizeof msgset);
719 num_msgs = num_tokens(msgset, ',');
721 cprintf("%d Nothing to do.\n", CIT_OK);
725 extract_token(targ, args, 1, '|', sizeof targ);
726 convert_room_name_macros(targ, sizeof targ);
727 targ[ROOMNAMELEN - 1] = 0;
728 is_copy = extract_int(args, 2);
730 if (CtdlGetRoom(&qtemp, targ) != 0) {
731 cprintf("%d '%s' does not exist.\n", ERROR + ROOM_NOT_FOUND, targ);
735 if (!strcasecmp(qtemp.QRname, CC->room.QRname)) {
736 cprintf("%d Source and target rooms are the same.\n", ERROR + ALREADY_EXISTS);
740 CtdlGetUser(&CC->user, CC->curr_user);
741 CtdlRoomAccess(&qtemp, &CC->user, &ra, NULL);
743 // Check for permission to perform this operation.
744 // Remember: "CC->room" is source, "qtemp" is target.
747 // Admins can move/copy
748 if (CC->user.axlevel >= AxAideU) permit = 1;
750 // Room aides can move/copy
751 if (CC->user.usernum == CC->room.QRroomaide) permit = 1;
753 // Permit move/copy from personal rooms
754 if ( (CC->room.QRflags & QR_MAILBOX)
755 && (qtemp.QRflags & QR_MAILBOX)
760 // Permit only copy from public to personal room
762 && (!(CC->room.QRflags & QR_MAILBOX))
763 && (qtemp.QRflags & QR_MAILBOX)
768 // Permit message removal from collaborative delete rooms
769 if (CC->room.QRflags2 & QR2_COLLABDEL) permit = 1;
771 // Users allowed to post into the target room may move into it too.
772 if ( (CC->room.QRflags & QR_MAILBOX)
773 && (qtemp.QRflags & UA_POSTALLOWED)
778 // User must have access to target room
779 if (!(ra & UA_KNOWN)) {
784 cprintf("%d Higher access required.\n", ERROR + HIGHER_ACCESS_REQUIRED);
788 // Build our message set to be moved/copied
789 msgs = malloc(num_msgs * sizeof(long));
790 for (i=0; i<num_msgs; ++i) {
791 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
792 msgs[i] = atol(msgtok);
796 err = CtdlSaveMsgPointersInRoom(targ, msgs, num_msgs, 1, NULL, 0);
798 cprintf("%d Cannot store message(s) in %s: error %d\n", err, targ, err);
803 // Now delete the message from the source room, if this is a 'move' rather than a 'copy' operation.
805 CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
809 cprintf("%d Message(s) %s.\n", CIT_OK, (is_copy ? "copied" : "moved") );
813 // Initialization function, called from modules_init.c
814 char *ctdl_module_init_ctdl_message(void) {
816 CtdlRegisterProtoHook(cmd_msgs, "MSGS", "Output a list of messages in the current room");
817 CtdlRegisterProtoHook(cmd_msg0, "MSG0", "Output a message in plain text format");
818 CtdlRegisterProtoHook(cmd_msg2, "MSG2", "Output a message in RFC822 format");
819 CtdlRegisterProtoHook(cmd_msg4, "MSG4", "Output a message in the client's preferred format");
820 CtdlRegisterProtoHook(cmd_msgp, "MSGP", "Select preferred format for MSG4 output");
821 CtdlRegisterProtoHook(cmd_opna, "OPNA", "Open an attachment for download");
822 CtdlRegisterProtoHook(cmd_dlat, "DLAT", "Download an attachment");
823 CtdlRegisterProtoHook(cmd_ent0, "ENT0", "Enter a message");
824 CtdlRegisterProtoHook(cmd_dele, "DELE", "Delete a message");
825 CtdlRegisterProtoHook(cmd_move, "MOVE", "Move or copy a message to another room");
828 // return a module name for the log
829 return "ctdl_message";