1 // Message-related protocol commands for Citadel clients
3 // Copyright (c) 1987-2021 by the citadel.org team
5 // This program is open source software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License version 3.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
14 #include <libcitadel.h>
15 #include "citserver.h"
16 #include "ctdl_module.h"
17 #include "internet_addressing.h"
22 extern char *msgkeys[];
25 // Back end for the MSGS command: output message number only.
26 void simple_listing(long msgnum, void *userdata) {
27 cprintf("%ld\n", msgnum);
31 // Back end for the MSGS command: output header summary.
32 void headers_listing(long msgnum, void *userdata) {
33 struct CtdlMessage *msg;
34 int output_mode = *(int *)userdata;
36 msg = CtdlFetchMessage(msgnum, 0);
38 cprintf("%ld|0|||||||\n", msgnum);
42 // change all vertical bars in the subject to hyphens so it doesn't screw up the protocol
43 if (!CM_IsEmpty(msg, eMsgSubject)) {
45 for (p=msg->cm_fields[eMsgSubject]; *p; p++) {
52 // output all fields except the references hash
53 cprintf("%ld|%s|%s|%s|%s|%s",
55 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"),
56 (!CM_IsEmpty(msg, eAuthor) ? msg->cm_fields[eAuthor] : ""),
57 CtdlGetConfigStr("c_nodename"), // no more nodenames anymore
58 (!CM_IsEmpty(msg, erFc822Addr) ? msg->cm_fields[erFc822Addr] : ""),
59 (!CM_IsEmpty(msg, eMsgSubject) ? msg->cm_fields[eMsgSubject] : "")
62 if (output_mode == MSG_HDRS_THREADS) { // field view with thread hashes
64 // output the references hash
66 (!CM_IsEmpty(msg, emessageId) ? HashLittle(msg->cm_fields[emessageId],strlen(msg->cm_fields[emessageId])) : 0)
69 // output the references hash (yes it's ok that we're trashing the source buffer by doing this)
70 if (!CM_IsEmpty(msg, eWeferences)) {
72 char *rest = msg->cm_fields[eWeferences];
74 while((token = strtok_r(rest, "|", &rest))) {
75 cprintf("%d%s", HashLittle(token,rest-prev-(*rest==0?0:1)), (*rest==0?"":","));
83 else { // field view with no threads, subject extends out forever
90 typedef struct _msg_filter {
97 void headers_brief_filter(long msgnum, void *userdata) {
99 struct CtdlMessage *msg;
100 msg_filter *flt = (msg_filter*) userdata;
102 l = GetCount(flt->Filter);
103 msg = CtdlFetchMessage(msgnum, 0);
104 StrBufPrintf(flt->buffer, "%ld", msgnum);
106 for (i = 0; i < l; i++) {
107 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
114 RewindHashPos(flt->Filter, flt->p, 0);
115 while (GetNextHashPos(flt->Filter, flt->p, &len, &k, &v)) {
116 eMsgField f = (eMsgField) v;
118 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
119 if (!CM_IsEmpty(msg, f)) {
120 StrBufAppendBufPlain(flt->buffer, CM_KEY(msg, f), 0);
124 StrBufAppendBufPlain(flt->buffer, HKEY("\n"), 0);
125 cputbuf(flt->buffer);
128 // Back end for the MSGS command: output EUID header.
129 void headers_euid(long msgnum, void *userdata) {
130 struct CtdlMessage *msg;
132 msg = CtdlFetchMessage(msgnum, 0);
134 cprintf("%ld||\n", msgnum);
138 cprintf("%ld|%s|%s\n",
140 (!CM_IsEmpty(msg, eExclusiveID) ? msg->cm_fields[eExclusiveID] : ""),
141 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"));
146 // cmd_msgs() - get list of message #'s in this room
147 // implements the MSGS server command using CtdlForEachMessage()
148 void cmd_msgs(char *cmdbuf) {
155 int with_template = 0;
156 struct CtdlMessage *template = NULL;
158 char search_string[1024];
159 ForEachMsgCallback CallBack;
161 if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
163 extract_token(which, cmdbuf, 0, '|', sizeof which);
164 cm_ref = extract_int(cmdbuf, 1);
165 extract_token(search_string, cmdbuf, 1, '|', sizeof search_string);
166 with_template = extract_int(cmdbuf, 2);
167 int output_mode = extract_int(cmdbuf, 3);
168 switch (output_mode) {
171 CallBack = simple_listing;
174 case MSG_HDRS_THREADS:
175 CallBack = headers_listing;
178 CallBack = headers_euid;
180 case MSG_HDRS_BRIEFFILTER:
182 CallBack = headers_brief_filter;
187 if (!strncasecmp(which, "OLD", 3))
189 else if (!strncasecmp(which, "NEW", 3))
191 else if (!strncasecmp(which, "FIRST", 5))
193 else if (!strncasecmp(which, "LAST", 4))
195 else if (!strncasecmp(which, "GT", 2))
197 else if (!strncasecmp(which, "LT", 2))
199 else if (!strncasecmp(which, "SEARCH", 6))
204 if ( (mode == MSGS_SEARCH) && (!CtdlGetConfigInt("c_enable_fulltext")) ) {
205 cprintf("%d Full text index is not enabled on this server.\n",
206 ERROR + CMD_NOT_SUPPORTED);
210 if (with_template == 1) {
213 cprintf("%d Send template then receive message list\n",
215 template = (struct CtdlMessage *)
216 malloc(sizeof(struct CtdlMessage));
217 memset(template, 0, sizeof(struct CtdlMessage));
218 template->cm_magic = CTDLMESSAGE_MAGIC;
219 template->cm_anon_type = MES_NORMAL;
221 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
225 tValueLen = extract_token(tfield, buf, 0, '|', sizeof tfield);
226 if ((tValueLen == 4) && GetFieldFromMnemonic(&f, tfield))
228 tValueLen = extract_token(tvalue, buf, 1, '|', sizeof tvalue);
229 if (tValueLen >= 0) {
230 CM_SetField(template, f, tvalue, tValueLen);
236 else if (with_template == 2) {
239 cprintf("%d Send list of headers\n",
241 filt.Filter = NewHash(1, lFlathash);
242 filt.buffer = NewStrBufPlain(NULL, 1024);
243 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
246 if (GetFieldFromMnemonic(&f, buf))
248 Put(filt.Filter, LKEY(i), (void*)f, reference_free_handler);
252 filt.p = GetNewHashPos(filt.Filter, 0);
256 cprintf("%d \n", LISTING_FOLLOWS);
259 if (with_template < 2) {
260 CtdlForEachMessage(mode,
261 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
262 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
267 if (template != NULL) CM_Free(template);
270 CtdlForEachMessage(mode,
271 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
272 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
277 DeleteHashPos(&filt.p);
278 DeleteHash(&filt.Filter);
279 FreeStrBuf(&filt.buffer);
287 * display a message (mode 0 - Citadel proprietary)
289 void cmd_msg0(char *cmdbuf)
292 int headers_only = HEADERS_ALL;
294 msgid = extract_long(cmdbuf, 0);
295 headers_only = extract_int(cmdbuf, 1);
297 CtdlOutputMsg(msgid, MT_CITADEL, headers_only, 1, 0, NULL, 0, NULL, NULL, NULL);
302 // display a message (mode 2 - RFC822)
303 void cmd_msg2(char *cmdbuf) {
305 int headers_only = HEADERS_ALL;
307 msgid = extract_long(cmdbuf, 0);
308 headers_only = extract_int(cmdbuf, 1);
310 CtdlOutputMsg(msgid, MT_RFC822, headers_only, 1, 1, NULL, 0, NULL, NULL, NULL);
314 // Display a message using MIME content types
315 void cmd_msg4(char *cmdbuf) {
319 msgid = extract_long(cmdbuf, 0);
320 extract_token(section, cmdbuf, 1, '|', sizeof section);
321 CtdlOutputMsg(msgid, MT_MIME, 0, 1, 0, (section[0] ? section : NULL) , 0, NULL, NULL, NULL);
325 // Client tells us its preferred message format(s)
326 void cmd_msgp(char *cmdbuf) {
327 if (!strcasecmp(cmdbuf, "dont_decode")) {
328 CC->msg4_dont_decode = 1;
329 cprintf("%d MSG4 will not pre-decode messages.\n", CIT_OK);
332 safestrncpy(CC->preferred_formats, cmdbuf, sizeof(CC->preferred_formats));
333 cprintf("%d Preferred MIME formats have been set.\n", CIT_OK);
338 // Open a component of a MIME message as a download file
339 void cmd_opna(char *cmdbuf) {
341 char desired_section[128];
343 msgid = extract_long(cmdbuf, 0);
344 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
345 safestrncpy(CC->download_desired_section, desired_section,
346 sizeof CC->download_desired_section);
347 CtdlOutputMsg(msgid, MT_DOWNLOAD, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
351 // Open a component of a MIME message and transmit it all at once
352 void cmd_dlat(char *cmdbuf) {
354 char desired_section[128];
356 msgid = extract_long(cmdbuf, 0);
357 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
358 safestrncpy(CC->download_desired_section, desired_section,
359 sizeof CC->download_desired_section);
360 CtdlOutputMsg(msgid, MT_SPEW_SECTION, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
364 // message entry - mode 0 (normal)
365 void cmd_ent0(char *entargs) {
370 char supplied_euid[128];
373 char newusername[256];
374 char newuseremail[256];
375 struct CtdlMessage *msg;
379 struct recptypes *valid = NULL;
380 struct recptypes *valid_to = NULL;
381 struct recptypes *valid_cc = NULL;
382 struct recptypes *valid_bcc = NULL;
384 int subject_required = 0;
389 int newuseremail_ok = 0;
390 char references[SIZ];
395 post = extract_int(entargs, 0);
396 extract_token(recp, entargs, 1, '|', sizeof recp);
397 anon_flag = extract_int(entargs, 2);
398 format_type = extract_int(entargs, 3);
399 extract_token(subject, entargs, 4, '|', sizeof subject);
400 extract_token(newusername, entargs, 5, '|', sizeof newusername);
401 do_confirm = extract_int(entargs, 6);
402 extract_token(cc, entargs, 7, '|', sizeof cc);
403 extract_token(bcc, entargs, 8, '|', sizeof bcc);
404 switch(CC->room.QRdefaultview) {
407 extract_token(supplied_euid, entargs, 9, '|', sizeof supplied_euid);
410 supplied_euid[0] = 0;
413 extract_token(newuseremail, entargs, 10, '|', sizeof newuseremail);
414 extract_token(references, entargs, 11, '|', sizeof references);
415 for (ptr=references; *ptr != 0; ++ptr) {
416 if (*ptr == '!') *ptr = '|';
419 /* first check to make sure the request is valid. */
421 err = CtdlDoIHavePermissionToPostInThisRoom(
426 (!IsEmptyStr(references)) /* is this a reply? or a top-level post? */
429 cprintf("%d %s\n", err, errmsg);
433 /* Check some other permission type things. */
435 if (IsEmptyStr(newusername)) {
436 strcpy(newusername, CC->user.fullname);
438 if ( (CC->user.axlevel < AxAideU)
439 && (strcasecmp(newusername, CC->user.fullname))
440 && (strcasecmp(newusername, CC->cs_inet_fn))
442 cprintf("%d You don't have permission to author messages as '%s'.\n",
443 ERROR + HIGHER_ACCESS_REQUIRED,
449 if (IsEmptyStr(newuseremail)) {
453 if (!IsEmptyStr(newuseremail)) {
454 if (!strcasecmp(newuseremail, CC->cs_inet_email)) {
457 else if (!IsEmptyStr(CC->cs_inet_other_emails)) {
458 j = num_tokens(CC->cs_inet_other_emails, '|');
459 for (i=0; i<j; ++i) {
460 extract_token(buf, CC->cs_inet_other_emails, i, '|', sizeof buf);
461 if (!strcasecmp(newuseremail, buf)) {
468 if (!newuseremail_ok) {
469 cprintf("%d You don't have permission to author messages as '%s'.\n",
470 ERROR + HIGHER_ACCESS_REQUIRED,
476 CC->cs_flags |= CS_POSTING;
478 // In mailbox rooms we have to behave a little differently --
479 // make sure the user has specified at least one recipient. Then
480 // validate the recipient(s). We do this for the Mail> room, as
481 // well as any room which has the "Mailbox" view set - unless it
482 // is the DRAFTS room which does not require recipients.
484 if ( ( ( (CC->room.QRflags & QR_MAILBOX) && (!strcasecmp(&CC->room.QRname[11], MAILROOM)) )
485 || ( (CC->room.QRflags & QR_MAILBOX) && (CC->curr_view == VIEW_MAILBOX) )
486 ) && (strcasecmp(&CC->room.QRname[11], USERDRAFTROOM)) !=0 ) {
487 if (CC->user.axlevel < AxProbU) {
488 strcpy(recp, "sysop");
494 valid_to = validate_recipients(recp, NULL, 0);
496 if (valid_to->num_error > 0) {
497 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_to->errormsg);
498 free_recipients(valid_to);
503 valid_cc = validate_recipients(cc, NULL, 0);
505 if (valid_cc->num_error > 0) {
506 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_cc->errormsg);
507 free_recipients(valid_to);
508 free_recipients(valid_cc);
513 valid_bcc = validate_recipients(bcc, NULL, 0);
515 if (valid_bcc->num_error > 0) {
516 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_bcc->errormsg);
517 free_recipients(valid_to);
518 free_recipients(valid_cc);
519 free_recipients(valid_bcc);
523 /* Recipient required, but none were specified */
524 if ( (valid_to->num_error < 0) && (valid_cc->num_error < 0) && (valid_bcc->num_error < 0) ) {
525 free_recipients(valid_to);
526 free_recipients(valid_cc);
527 free_recipients(valid_bcc);
528 cprintf("%d At least one recipient is required.\n", ERROR + NO_SUCH_USER);
532 if (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0) {
533 if (CtdlCheckInternetMailPermission(&CC->user)==0) {
534 cprintf("%d You do not have permission "
535 "to send Internet mail.\n",
536 ERROR + HIGHER_ACCESS_REQUIRED);
537 free_recipients(valid_to);
538 free_recipients(valid_cc);
539 free_recipients(valid_bcc);
544 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)
545 && (CC->user.axlevel < AxNetU) ) {
546 cprintf("%d Higher access required for network mail.\n",
547 ERROR + HIGHER_ACCESS_REQUIRED);
548 free_recipients(valid_to);
549 free_recipients(valid_cc);
550 free_recipients(valid_bcc);
554 if ((RESTRICT_INTERNET == 1)
555 && (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0)
556 && ((CC->user.flags & US_INTERNET) == 0)
557 && (!CC->internal_pgm)) {
558 cprintf("%d You don't have access to Internet mail.\n",
559 ERROR + HIGHER_ACCESS_REQUIRED);
560 free_recipients(valid_to);
561 free_recipients(valid_cc);
562 free_recipients(valid_bcc);
568 /* Is this a room which has anonymous-only or anonymous-option? */
569 anonymous = MES_NORMAL;
570 if (CC->room.QRflags & QR_ANONONLY) {
571 anonymous = MES_ANONONLY;
573 if (CC->room.QRflags & QR_ANONOPT) {
574 if (anon_flag == 1) { /* only if the user requested it */
575 anonymous = MES_ANONOPT;
579 if ((CC->room.QRflags & QR_MAILBOX) == 0) {
583 /* Recommend to the client that the use of a message subject is
584 * strongly recommended in this room, if either the SUBJECTREQ flag
585 * is set, or if there is one or more Internet email recipients.
587 if (CC->room.QRflags2 & QR2_SUBJECTREQ) subject_required = 1;
588 if ((valid_to) && (valid_to->num_internet > 0)) subject_required = 1;
589 if ((valid_cc) && (valid_cc->num_internet > 0)) subject_required = 1;
590 if ((valid_bcc) && (valid_bcc->num_internet > 0)) subject_required = 1;
592 /* If we're only checking the validity of the request, return
593 * success without creating the message.
596 cprintf("%d %s|%d\n", CIT_OK,
597 ((valid_to != NULL) ? valid_to->display_recp : ""),
599 free_recipients(valid_to);
600 free_recipients(valid_cc);
601 free_recipients(valid_bcc);
605 /* We don't need these anymore because we'll do it differently below */
606 free_recipients(valid_to);
607 free_recipients(valid_cc);
608 free_recipients(valid_bcc);
610 /* Read in the message from the client. */
612 cprintf("%d send message\n", START_CHAT_MODE);
614 cprintf("%d send message\n", SEND_LISTING);
617 msg = CtdlMakeMessage(&CC->user, recp, cc,
618 CC->room.QRname, anonymous, format_type,
619 newusername, newuseremail, subject,
620 ((!IsEmptyStr(supplied_euid)) ? supplied_euid : NULL),
623 /* Put together one big recipients struct containing to/cc/bcc all in
624 * one. This is for the envelope.
626 char *all_recps = malloc(SIZ * 3);
627 strcpy(all_recps, recp);
628 if (!IsEmptyStr(cc)) {
629 if (!IsEmptyStr(all_recps)) {
630 strcat(all_recps, ",");
632 strcat(all_recps, cc);
634 if (!IsEmptyStr(bcc)) {
635 if (!IsEmptyStr(all_recps)) {
636 strcat(all_recps, ",");
638 strcat(all_recps, bcc);
640 if (!IsEmptyStr(all_recps)) {
642 valid = validate_recipients(all_recps, NULL, 0);
650 if ((valid != NULL) && (valid->num_room == 1) && !IsEmptyStr(valid->recp_orgroom))
652 /* posting into an ML room? set the envelope from
653 * to the actual mail address so others get a valid
656 CM_SetField(msg, eenVelopeTo, valid->recp_orgroom, strlen(valid->recp_orgroom));
660 msgnum = CtdlSubmitMsg(msg, valid, "");
662 cprintf("%ld\n", msgnum);
664 if (StrLength(CC->StatusMessage) > 0) {
665 cprintf("%s\n", ChrPtr(CC->StatusMessage));
667 else if (msgnum >= 0L) {
668 client_write(HKEY("Message accepted.\n"));
671 client_write(HKEY("Internal error.\n"));
674 if (!CM_IsEmpty(msg, eExclusiveID)) {
675 cprintf("%s\n", msg->cm_fields[eExclusiveID]);
685 free_recipients(valid);
691 // Delete message from current room
692 void cmd_dele(char *args) {
700 extract_token(msgset, args, 0, '|', sizeof msgset);
701 num_msgs = num_tokens(msgset, ',');
703 cprintf("%d Nothing to do.\n", CIT_OK);
707 if (CtdlDoIHavePermissionToDeleteMessagesFromThisRoom() == 0) {
708 cprintf("%d Higher access required.\n",
709 ERROR + HIGHER_ACCESS_REQUIRED);
714 * Build our message set to be moved/copied
716 msgs = malloc(num_msgs * sizeof(long));
717 for (i=0; i<num_msgs; ++i) {
718 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
719 msgs[i] = atol(msgtok);
722 num_deleted = CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
726 cprintf("%d %d message%s deleted.\n", CIT_OK,
727 num_deleted, ((num_deleted != 1) ? "s" : ""));
729 cprintf("%d Message not found.\n", ERROR + MESSAGE_NOT_FOUND);
734 // move or copy a message to another room
735 void cmd_move(char *args) {
741 char targ[ROOMNAMELEN];
742 struct ctdlroom qtemp;
749 extract_token(msgset, args, 0, '|', sizeof msgset);
750 num_msgs = num_tokens(msgset, ',');
752 cprintf("%d Nothing to do.\n", CIT_OK);
756 extract_token(targ, args, 1, '|', sizeof targ);
757 convert_room_name_macros(targ, sizeof targ);
758 targ[ROOMNAMELEN - 1] = 0;
759 is_copy = extract_int(args, 2);
761 if (CtdlGetRoom(&qtemp, targ) != 0) {
762 cprintf("%d '%s' does not exist.\n", ERROR + ROOM_NOT_FOUND, targ);
766 if (!strcasecmp(qtemp.QRname, CC->room.QRname)) {
767 cprintf("%d Source and target rooms are the same.\n", ERROR + ALREADY_EXISTS);
771 CtdlGetUser(&CC->user, CC->curr_user);
772 CtdlRoomAccess(&qtemp, &CC->user, &ra, NULL);
774 /* Check for permission to perform this operation.
775 * Remember: "CC->room" is source, "qtemp" is target.
779 /* Admins can move/copy */
780 if (CC->user.axlevel >= AxAideU) permit = 1;
782 /* Room aides can move/copy */
783 if (CC->user.usernum == CC->room.QRroomaide) permit = 1;
785 /* Permit move/copy from personal rooms */
786 if ((CC->room.QRflags & QR_MAILBOX)
787 && (qtemp.QRflags & QR_MAILBOX)) permit = 1;
789 /* Permit only copy from public to personal room */
791 && (!(CC->room.QRflags & QR_MAILBOX))
792 && (qtemp.QRflags & QR_MAILBOX)) permit = 1;
794 /* Permit message removal from collaborative delete rooms */
795 if (CC->room.QRflags2 & QR2_COLLABDEL) permit = 1;
797 /* Users allowed to post into the target room may move into it too. */
798 if ((CC->room.QRflags & QR_MAILBOX) &&
799 (qtemp.QRflags & UA_POSTALLOWED)) permit = 1;
801 /* User must have access to target room */
802 if (!(ra & UA_KNOWN)) permit = 0;
805 cprintf("%d Higher access required.\n",
806 ERROR + HIGHER_ACCESS_REQUIRED);
811 * Build our message set to be moved/copied
813 msgs = malloc(num_msgs * sizeof(long));
814 for (i=0; i<num_msgs; ++i) {
815 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
816 msgs[i] = atol(msgtok);
822 err = CtdlSaveMsgPointersInRoom(targ, msgs, num_msgs, 1, NULL, 0);
824 cprintf("%d Cannot store message(s) in %s: error %d\n",
830 /* Now delete the message from the source room,
831 * if this is a 'move' rather than a 'copy' operation.
834 CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
838 cprintf("%d Message(s) %s.\n", CIT_OK, (is_copy ? "copied" : "moved") );
842 /*****************************************************************************/
843 /* MODULE INITIALIZATION STUFF */
844 /*****************************************************************************/
845 CTDL_MODULE_INIT(ctdl_message)
849 CtdlRegisterProtoHook(cmd_msgs, "MSGS", "Output a list of messages in the current room");
850 CtdlRegisterProtoHook(cmd_msg0, "MSG0", "Output a message in plain text format");
851 CtdlRegisterProtoHook(cmd_msg2, "MSG2", "Output a message in RFC822 format");
852 CtdlRegisterProtoHook(cmd_msg4, "MSG4", "Output a message in the client's preferred format");
853 CtdlRegisterProtoHook(cmd_msgp, "MSGP", "Select preferred format for MSG4 output");
854 CtdlRegisterProtoHook(cmd_opna, "OPNA", "Open an attachment for download");
855 CtdlRegisterProtoHook(cmd_dlat, "DLAT", "Download an attachment");
856 CtdlRegisterProtoHook(cmd_ent0, "ENT0", "Enter a message");
857 CtdlRegisterProtoHook(cmd_dele, "DELE", "Delete a message");
858 CtdlRegisterProtoHook(cmd_move, "MOVE", "Move or copy a message to another room");
861 /* return our Subversion id for the Log */
862 return "ctdl_message";