2 * represent messages to the citadel clients
4 * Copyright (c) 1987-2019 by the citadel.org team
6 * This program is open source software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License version 3.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
16 #include <libcitadel.h>
18 #include "citserver.h"
19 #include "ctdl_module.h"
20 #include "internet_addressing.h"
25 extern char *msgkeys[];
29 * Back end for the MSGS command: output message number only.
31 void simple_listing(long msgnum, void *userdata)
33 cprintf("%ld\n", msgnum);
38 * Back end for the MSGS command: output header summary.
40 void headers_listing(long msgnum, void *userdata)
42 struct CtdlMessage *msg;
43 int output_mode = *(int *)userdata;
45 msg = CtdlFetchMessage(msgnum, 0, 1);
47 cprintf("%ld|0|||||||\n", msgnum);
51 // change all vertical bars in the subject to hyphens so it doesn't screw up the protocol
52 if (!CM_IsEmpty(msg, eMsgSubject)) {
54 for (p=msg->cm_fields[eMsgSubject]; *p; p++) {
61 // output all fields except the references hash
62 cprintf("%ld|%s|%s|%s|%s|%s",
64 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"),
65 (!CM_IsEmpty(msg, eAuthor) ? msg->cm_fields[eAuthor] : ""),
66 CtdlGetConfigStr("c_nodename"), // no more nodenames anymore
67 (!CM_IsEmpty(msg, erFc822Addr) ? msg->cm_fields[erFc822Addr] : ""),
68 (!CM_IsEmpty(msg, eMsgSubject) ? msg->cm_fields[eMsgSubject] : "")
71 if (output_mode == MSG_HDRS_THREADS) { // field view with thread hashes
73 // output the references hash
75 (!CM_IsEmpty(msg, emessageId) ? HashLittle(msg->cm_fields[emessageId],strlen(msg->cm_fields[emessageId])) : 0)
78 // output the references hash (yes it's ok that we're trashing the source buffer by doing this)
79 if (!CM_IsEmpty(msg, eWeferences)) {
81 char *rest = msg->cm_fields[eWeferences];
83 while((token = strtok_r(rest, "|", &rest))) {
84 cprintf("%d%s", HashLittle(token,rest-prev-(*rest==0?0:1)), (*rest==0?"":","));
92 else { // field view with no threads, subject extends out forever
99 typedef struct _msg_filter{
105 void headers_brief_filter(long msgnum, void *userdata)
108 struct CtdlMessage *msg;
109 msg_filter *flt = (msg_filter*) userdata;
111 l = GetCount(flt->Filter);
112 msg = CtdlFetchMessage(msgnum, 0, 1);
113 StrBufPrintf(flt->buffer, "%ld", msgnum);
115 for (i = 0; i < l; i++) {
116 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
123 RewindHashPos(flt->Filter, flt->p, 0);
124 while (GetNextHashPos(flt->Filter, flt->p, &len, &k, &v)) {
125 eMsgField f = (eMsgField) v;
127 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
128 if (!CM_IsEmpty(msg, f)) {
129 StrBufAppendBufPlain(flt->buffer, CM_KEY(msg, f), 0);
133 StrBufAppendBufPlain(flt->buffer, HKEY("\n"), 0);
134 cputbuf(flt->buffer);
138 * Back end for the MSGS command: output EUID header.
140 void headers_euid(long msgnum, void *userdata)
142 struct CtdlMessage *msg;
144 msg = CtdlFetchMessage(msgnum, 0, 1);
146 cprintf("%ld||\n", msgnum);
150 cprintf("%ld|%s|%s\n",
152 (!CM_IsEmpty(msg, eExclusiveID) ? msg->cm_fields[eExclusiveID] : ""),
153 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"));
160 * cmd_msgs() - get list of message #'s in this room
161 * implements the MSGS server command using CtdlForEachMessage()
163 void cmd_msgs(char *cmdbuf)
171 int with_template = 0;
172 struct CtdlMessage *template = NULL;
174 char search_string[1024];
175 ForEachMsgCallback CallBack;
177 if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
179 extract_token(which, cmdbuf, 0, '|', sizeof which);
180 cm_ref = extract_int(cmdbuf, 1);
181 extract_token(search_string, cmdbuf, 1, '|', sizeof search_string);
182 with_template = extract_int(cmdbuf, 2);
183 int output_mode = extract_int(cmdbuf, 3);
184 switch (output_mode) {
187 CallBack = simple_listing;
190 case MSG_HDRS_THREADS:
191 CallBack = headers_listing;
194 CallBack = headers_euid;
196 case MSG_HDRS_BRIEFFILTER:
198 CallBack = headers_brief_filter;
203 if (!strncasecmp(which, "OLD", 3))
205 else if (!strncasecmp(which, "NEW", 3))
207 else if (!strncasecmp(which, "FIRST", 5))
209 else if (!strncasecmp(which, "LAST", 4))
211 else if (!strncasecmp(which, "GT", 2))
213 else if (!strncasecmp(which, "LT", 2))
215 else if (!strncasecmp(which, "SEARCH", 6))
220 if ( (mode == MSGS_SEARCH) && (!CtdlGetConfigInt("c_enable_fulltext")) ) {
221 cprintf("%d Full text index is not enabled on this server.\n",
222 ERROR + CMD_NOT_SUPPORTED);
226 if (with_template == 1) {
229 cprintf("%d Send template then receive message list\n",
231 template = (struct CtdlMessage *)
232 malloc(sizeof(struct CtdlMessage));
233 memset(template, 0, sizeof(struct CtdlMessage));
234 template->cm_magic = CTDLMESSAGE_MAGIC;
235 template->cm_anon_type = MES_NORMAL;
237 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
241 tValueLen = extract_token(tfield, buf, 0, '|', sizeof tfield);
242 if ((tValueLen == 4) && GetFieldFromMnemonic(&f, tfield))
244 tValueLen = extract_token(tvalue, buf, 1, '|', sizeof tvalue);
245 if (tValueLen >= 0) {
246 CM_SetField(template, f, tvalue, tValueLen);
252 else if (with_template == 2) {
255 cprintf("%d Send list of headers\n",
257 filt.Filter = NewHash(1, lFlathash);
258 filt.buffer = NewStrBufPlain(NULL, 1024);
259 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
262 if (GetFieldFromMnemonic(&f, buf))
264 Put(filt.Filter, LKEY(i), (void*)f, reference_free_handler);
268 filt.p = GetNewHashPos(filt.Filter, 0);
272 cprintf("%d \n", LISTING_FOLLOWS);
275 if (with_template < 2) {
276 CtdlForEachMessage(mode,
277 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
278 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
283 if (template != NULL) CM_Free(template);
286 CtdlForEachMessage(mode,
287 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
288 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
293 DeleteHashPos(&filt.p);
294 DeleteHash(&filt.Filter);
295 FreeStrBuf(&filt.buffer);
302 * display a message (mode 0 - Citadel proprietary)
304 void cmd_msg0(char *cmdbuf)
307 int headers_only = HEADERS_ALL;
309 msgid = extract_long(cmdbuf, 0);
310 headers_only = extract_int(cmdbuf, 1);
312 CtdlOutputMsg(msgid, MT_CITADEL, headers_only, 1, 0, NULL, 0, NULL, NULL, NULL);
318 * display a message (mode 2 - RFC822)
320 void cmd_msg2(char *cmdbuf)
323 int headers_only = HEADERS_ALL;
325 msgid = extract_long(cmdbuf, 0);
326 headers_only = extract_int(cmdbuf, 1);
328 CtdlOutputMsg(msgid, MT_RFC822, headers_only, 1, 1, NULL, 0, NULL, NULL, NULL);
333 * Display a message using MIME content types
335 void cmd_msg4(char *cmdbuf)
340 msgid = extract_long(cmdbuf, 0);
341 extract_token(section, cmdbuf, 1, '|', sizeof section);
342 CtdlOutputMsg(msgid, MT_MIME, 0, 1, 0, (section[0] ? section : NULL) , 0, NULL, NULL, NULL);
347 * Client tells us its preferred message format(s)
349 void cmd_msgp(char *cmdbuf)
351 if (!strcasecmp(cmdbuf, "dont_decode")) {
352 CC->msg4_dont_decode = 1;
353 cprintf("%d MSG4 will not pre-decode messages.\n", CIT_OK);
356 safestrncpy(CC->preferred_formats, cmdbuf, sizeof(CC->preferred_formats));
357 cprintf("%d Preferred MIME formats have been set.\n", CIT_OK);
363 * Open a component of a MIME message as a download file
365 void cmd_opna(char *cmdbuf)
368 char desired_section[128];
370 msgid = extract_long(cmdbuf, 0);
371 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
372 safestrncpy(CC->download_desired_section, desired_section,
373 sizeof CC->download_desired_section);
374 CtdlOutputMsg(msgid, MT_DOWNLOAD, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
379 * Open a component of a MIME message and transmit it all at once
381 void cmd_dlat(char *cmdbuf)
384 char desired_section[128];
386 msgid = extract_long(cmdbuf, 0);
387 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
388 safestrncpy(CC->download_desired_section, desired_section,
389 sizeof CC->download_desired_section);
390 CtdlOutputMsg(msgid, MT_SPEW_SECTION, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
394 * message entry - mode 0 (normal)
396 void cmd_ent0(char *entargs)
398 struct CitContext *CCC = CC;
403 char supplied_euid[128];
406 char newusername[256];
407 char newuseremail[256];
408 struct CtdlMessage *msg;
412 recptypes *valid = NULL;
413 recptypes *valid_to = NULL;
414 recptypes *valid_cc = NULL;
415 recptypes *valid_bcc = NULL;
417 int subject_required = 0;
422 int newuseremail_ok = 0;
423 char references[SIZ];
428 post = extract_int(entargs, 0);
429 extract_token(recp, entargs, 1, '|', sizeof recp);
430 anon_flag = extract_int(entargs, 2);
431 format_type = extract_int(entargs, 3);
432 extract_token(subject, entargs, 4, '|', sizeof subject);
433 extract_token(newusername, entargs, 5, '|', sizeof newusername);
434 do_confirm = extract_int(entargs, 6);
435 extract_token(cc, entargs, 7, '|', sizeof cc);
436 extract_token(bcc, entargs, 8, '|', sizeof bcc);
437 switch(CC->room.QRdefaultview) {
441 extract_token(supplied_euid, entargs, 9, '|', sizeof supplied_euid);
444 supplied_euid[0] = 0;
447 extract_token(newuseremail, entargs, 10, '|', sizeof newuseremail);
448 extract_token(references, entargs, 11, '|', sizeof references);
449 for (ptr=references; *ptr != 0; ++ptr) {
450 if (*ptr == '!') *ptr = '|';
453 /* first check to make sure the request is valid. */
455 err = CtdlDoIHavePermissionToPostInThisRoom(
460 (!IsEmptyStr(references)) /* is this a reply? or a top-level post? */
464 cprintf("%d %s\n", err, errmsg);
468 /* Check some other permission type things. */
470 if (IsEmptyStr(newusername)) {
471 strcpy(newusername, CCC->user.fullname);
473 if ( (CCC->user.axlevel < AxAideU)
474 && (strcasecmp(newusername, CCC->user.fullname))
475 && (strcasecmp(newusername, CCC->cs_inet_fn))
477 cprintf("%d You don't have permission to author messages as '%s'.\n",
478 ERROR + HIGHER_ACCESS_REQUIRED,
485 if (IsEmptyStr(newuseremail)) {
489 if (!IsEmptyStr(newuseremail)) {
490 if (!strcasecmp(newuseremail, CCC->cs_inet_email)) {
493 else if (!IsEmptyStr(CCC->cs_inet_other_emails)) {
494 j = num_tokens(CCC->cs_inet_other_emails, '|');
495 for (i=0; i<j; ++i) {
496 extract_token(buf, CCC->cs_inet_other_emails, i, '|', sizeof buf);
497 if (!strcasecmp(newuseremail, buf)) {
504 if (!newuseremail_ok) {
505 cprintf("%d You don't have permission to author messages as '%s'.\n",
506 ERROR + HIGHER_ACCESS_REQUIRED,
512 CCC->cs_flags |= CS_POSTING;
514 /* In mailbox rooms we have to behave a little differently --
515 * make sure the user has specified at least one recipient. Then
516 * validate the recipient(s). We do this for the Mail> room, as
517 * well as any room which has the "Mailbox" view set - unless it
518 * is the DRAFTS room which does not require recipients
521 if ( ( ( (CCC->room.QRflags & QR_MAILBOX) && (!strcasecmp(&CCC->room.QRname[11], MAILROOM)) )
522 || ( (CCC->room.QRflags & QR_MAILBOX) && (CCC->curr_view == VIEW_MAILBOX) )
523 ) && (strcasecmp(&CCC->room.QRname[11], USERDRAFTROOM)) !=0 ) {
524 if (CCC->user.axlevel < AxProbU) {
525 strcpy(recp, "sysop");
530 valid_to = validate_recipients(recp, NULL, 0);
531 if (valid_to->num_error > 0) {
532 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_to->errormsg);
533 free_recipients(valid_to);
537 valid_cc = validate_recipients(cc, NULL, 0);
538 if (valid_cc->num_error > 0) {
539 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_cc->errormsg);
540 free_recipients(valid_to);
541 free_recipients(valid_cc);
545 valid_bcc = validate_recipients(bcc, NULL, 0);
546 if (valid_bcc->num_error > 0) {
547 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_bcc->errormsg);
548 free_recipients(valid_to);
549 free_recipients(valid_cc);
550 free_recipients(valid_bcc);
554 /* Recipient required, but none were specified */
555 if ( (valid_to->num_error < 0) && (valid_cc->num_error < 0) && (valid_bcc->num_error < 0) ) {
556 free_recipients(valid_to);
557 free_recipients(valid_cc);
558 free_recipients(valid_bcc);
559 cprintf("%d At least one recipient is required.\n", ERROR + NO_SUCH_USER);
563 if (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0) {
564 if (CtdlCheckInternetMailPermission(&CCC->user)==0) {
565 cprintf("%d You do not have permission "
566 "to send Internet mail.\n",
567 ERROR + HIGHER_ACCESS_REQUIRED);
568 free_recipients(valid_to);
569 free_recipients(valid_cc);
570 free_recipients(valid_bcc);
575 if ( ( (valid_to->num_internet + valid_to->num_ignet + valid_cc->num_internet + valid_cc->num_ignet + valid_bcc->num_internet + valid_bcc->num_ignet) > 0)
576 && (CCC->user.axlevel < AxNetU) ) {
577 cprintf("%d Higher access required for network mail.\n",
578 ERROR + HIGHER_ACCESS_REQUIRED);
579 free_recipients(valid_to);
580 free_recipients(valid_cc);
581 free_recipients(valid_bcc);
585 if ((RESTRICT_INTERNET == 1)
586 && (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0)
587 && ((CCC->user.flags & US_INTERNET) == 0)
588 && (!CCC->internal_pgm)) {
589 cprintf("%d You don't have access to Internet mail.\n",
590 ERROR + HIGHER_ACCESS_REQUIRED);
591 free_recipients(valid_to);
592 free_recipients(valid_cc);
593 free_recipients(valid_bcc);
599 /* Is this a room which has anonymous-only or anonymous-option? */
600 anonymous = MES_NORMAL;
601 if (CCC->room.QRflags & QR_ANONONLY) {
602 anonymous = MES_ANONONLY;
604 if (CCC->room.QRflags & QR_ANONOPT) {
605 if (anon_flag == 1) { /* only if the user requested it */
606 anonymous = MES_ANONOPT;
610 if ((CCC->room.QRflags & QR_MAILBOX) == 0) {
614 /* Recommend to the client that the use of a message subject is
615 * strongly recommended in this room, if either the SUBJECTREQ flag
616 * is set, or if there is one or more Internet email recipients.
618 if (CCC->room.QRflags2 & QR2_SUBJECTREQ) subject_required = 1;
619 if ((valid_to) && (valid_to->num_internet > 0)) subject_required = 1;
620 if ((valid_cc) && (valid_cc->num_internet > 0)) subject_required = 1;
621 if ((valid_bcc) && (valid_bcc->num_internet > 0)) subject_required = 1;
623 /* If we're only checking the validity of the request, return
624 * success without creating the message.
627 cprintf("%d %s|%d\n", CIT_OK,
628 ((valid_to != NULL) ? valid_to->display_recp : ""),
630 free_recipients(valid_to);
631 free_recipients(valid_cc);
632 free_recipients(valid_bcc);
636 /* We don't need these anymore because we'll do it differently below */
637 free_recipients(valid_to);
638 free_recipients(valid_cc);
639 free_recipients(valid_bcc);
641 /* Read in the message from the client. */
643 cprintf("%d send message\n", START_CHAT_MODE);
645 cprintf("%d send message\n", SEND_LISTING);
648 msg = CtdlMakeMessage(&CCC->user, recp, cc,
649 CCC->room.QRname, anonymous, format_type,
650 newusername, newuseremail, subject,
651 ((!IsEmptyStr(supplied_euid)) ? supplied_euid : NULL),
654 /* Put together one big recipients struct containing to/cc/bcc all in
655 * one. This is for the envelope.
657 char *all_recps = malloc(SIZ * 3);
658 strcpy(all_recps, recp);
659 if (!IsEmptyStr(cc)) {
660 if (!IsEmptyStr(all_recps)) {
661 strcat(all_recps, ",");
663 strcat(all_recps, cc);
665 if (!IsEmptyStr(bcc)) {
666 if (!IsEmptyStr(all_recps)) {
667 strcat(all_recps, ",");
669 strcat(all_recps, bcc);
671 if (!IsEmptyStr(all_recps)) {
672 valid = validate_recipients(all_recps, NULL, 0);
679 if ((valid != NULL) && (valid->num_room == 1) && !IsEmptyStr(valid->recp_orgroom))
681 /* posting into an ML room? set the envelope from
682 * to the actual mail address so others get a valid
685 CM_SetField(msg, eenVelopeTo, valid->recp_orgroom, strlen(valid->recp_orgroom));
689 msgnum = CtdlSubmitMsg(msg, valid, "", QP_EADDR);
691 cprintf("%ld\n", msgnum);
693 if (StrLength(CCC->StatusMessage) > 0) {
694 cprintf("%s\n", ChrPtr(CCC->StatusMessage));
696 else if (msgnum >= 0L) {
697 client_write(HKEY("Message accepted.\n"));
700 client_write(HKEY("Internal error.\n"));
703 if (!CM_IsEmpty(msg, eExclusiveID)) {
704 cprintf("%s\n", msg->cm_fields[eExclusiveID]);
714 free_recipients(valid);
720 * Delete message from current room
722 void cmd_dele(char *args)
731 extract_token(msgset, args, 0, '|', sizeof msgset);
732 num_msgs = num_tokens(msgset, ',');
734 cprintf("%d Nothing to do.\n", CIT_OK);
738 if (CtdlDoIHavePermissionToDeleteMessagesFromThisRoom() == 0) {
739 cprintf("%d Higher access required.\n",
740 ERROR + HIGHER_ACCESS_REQUIRED);
745 * Build our message set to be moved/copied
747 msgs = malloc(num_msgs * sizeof(long));
748 for (i=0; i<num_msgs; ++i) {
749 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
750 msgs[i] = atol(msgtok);
753 num_deleted = CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
757 cprintf("%d %d message%s deleted.\n", CIT_OK,
758 num_deleted, ((num_deleted != 1) ? "s" : ""));
760 cprintf("%d Message not found.\n", ERROR + MESSAGE_NOT_FOUND);
767 * move or copy a message to another room
769 void cmd_move(char *args)
776 char targ[ROOMNAMELEN];
777 struct ctdlroom qtemp;
784 extract_token(msgset, args, 0, '|', sizeof msgset);
785 num_msgs = num_tokens(msgset, ',');
787 cprintf("%d Nothing to do.\n", CIT_OK);
791 extract_token(targ, args, 1, '|', sizeof targ);
792 convert_room_name_macros(targ, sizeof targ);
793 targ[ROOMNAMELEN - 1] = 0;
794 is_copy = extract_int(args, 2);
796 if (CtdlGetRoom(&qtemp, targ) != 0) {
797 cprintf("%d '%s' does not exist.\n", ERROR + ROOM_NOT_FOUND, targ);
801 if (!strcasecmp(qtemp.QRname, CC->room.QRname)) {
802 cprintf("%d Source and target rooms are the same.\n", ERROR + ALREADY_EXISTS);
806 CtdlGetUser(&CC->user, CC->curr_user);
807 CtdlRoomAccess(&qtemp, &CC->user, &ra, NULL);
809 /* Check for permission to perform this operation.
810 * Remember: "CC->room" is source, "qtemp" is target.
814 /* Admins can move/copy */
815 if (CC->user.axlevel >= AxAideU) permit = 1;
817 /* Room aides can move/copy */
818 if (CC->user.usernum == CC->room.QRroomaide) permit = 1;
820 /* Permit move/copy from personal rooms */
821 if ((CC->room.QRflags & QR_MAILBOX)
822 && (qtemp.QRflags & QR_MAILBOX)) permit = 1;
824 /* Permit only copy from public to personal room */
826 && (!(CC->room.QRflags & QR_MAILBOX))
827 && (qtemp.QRflags & QR_MAILBOX)) permit = 1;
829 /* Permit message removal from collaborative delete rooms */
830 if (CC->room.QRflags2 & QR2_COLLABDEL) permit = 1;
832 /* Users allowed to post into the target room may move into it too. */
833 if ((CC->room.QRflags & QR_MAILBOX) &&
834 (qtemp.QRflags & UA_POSTALLOWED)) permit = 1;
836 /* User must have access to target room */
837 if (!(ra & UA_KNOWN)) permit = 0;
840 cprintf("%d Higher access required.\n",
841 ERROR + HIGHER_ACCESS_REQUIRED);
846 * Build our message set to be moved/copied
848 msgs = malloc(num_msgs * sizeof(long));
849 for (i=0; i<num_msgs; ++i) {
850 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
851 msgs[i] = atol(msgtok);
857 err = CtdlSaveMsgPointersInRoom(targ, msgs, num_msgs, 1, NULL, 0);
859 cprintf("%d Cannot store message(s) in %s: error %d\n",
865 /* Now delete the message from the source room,
866 * if this is a 'move' rather than a 'copy' operation.
869 CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
873 cprintf("%d Message(s) %s.\n", CIT_OK, (is_copy ? "copied" : "moved") );
877 /*****************************************************************************/
878 /* MODULE INITIALIZATION STUFF */
879 /*****************************************************************************/
880 CTDL_MODULE_INIT(ctdl_message)
884 CtdlRegisterProtoHook(cmd_msgs, "MSGS", "Output a list of messages in the current room");
885 CtdlRegisterProtoHook(cmd_msg0, "MSG0", "Output a message in plain text format");
886 CtdlRegisterProtoHook(cmd_msg2, "MSG2", "Output a message in RFC822 format");
887 CtdlRegisterProtoHook(cmd_msg4, "MSG4", "Output a message in the client's preferred format");
888 CtdlRegisterProtoHook(cmd_msgp, "MSGP", "Select preferred format for MSG4 output");
889 CtdlRegisterProtoHook(cmd_opna, "OPNA", "Open an attachment for download");
890 CtdlRegisterProtoHook(cmd_dlat, "DLAT", "Download an attachment");
891 CtdlRegisterProtoHook(cmd_ent0, "ENT0", "Enter a message");
892 CtdlRegisterProtoHook(cmd_dele, "DELE", "Delete a message");
893 CtdlRegisterProtoHook(cmd_move, "MOVE", "Move or copy a message to another room");
896 /* return our Subversion id for the Log */
897 return "ctdl_message";