2 * represent messages to the citadel clients
4 * Copyright (c) 1987-2015 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;
44 msg = CtdlFetchMessage(msgnum, 0, 1);
46 cprintf("%ld|0|||||\n", msgnum);
50 cprintf("%ld|%s|%s|%s|%s|%s|\n",
52 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"),
53 (!CM_IsEmpty(msg, eAuthor) ? msg->cm_fields[eAuthor] : ""),
54 (!CM_IsEmpty(msg, eNodeName) ? msg->cm_fields[eNodeName] : ""),
55 (!CM_IsEmpty(msg, erFc822Addr) ? msg->cm_fields[erFc822Addr] : ""),
56 (!CM_IsEmpty(msg, eMsgSubject) ? msg->cm_fields[eMsgSubject] : "")
61 typedef struct _msg_filter{
67 void headers_brief_filter(long msgnum, void *userdata)
70 struct CtdlMessage *msg;
71 msg_filter *flt = (msg_filter*) userdata;
73 l = GetCount(flt->Filter);
74 msg = CtdlFetchMessage(msgnum, 0, 1);
75 StrBufPrintf(flt->buffer, "%ld", msgnum);
77 for (i = 0; i < l; i++) {
78 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
85 RewindHashPos(flt->Filter, flt->p, 0);
86 while (GetNextHashPos(flt->Filter, flt->p, &len, &k, &v)) {
87 eMsgField f = (eMsgField) v;
89 StrBufAppendBufPlain(flt->buffer, HKEY("|"), 0);
90 if (!CM_IsEmpty(msg, f)) {
91 StrBufAppendBufPlain(flt->buffer, CM_KEY(msg, f), 0);
95 StrBufAppendBufPlain(flt->buffer, HKEY("\n"), 0);
100 * Back end for the MSGS command: output EUID header.
102 void headers_euid(long msgnum, void *userdata)
104 struct CtdlMessage *msg;
106 msg = CtdlFetchMessage(msgnum, 0, 1);
108 cprintf("%ld||\n", msgnum);
112 cprintf("%ld|%s|%s\n",
114 (!CM_IsEmpty(msg, eExclusiveID) ? msg->cm_fields[eExclusiveID] : ""),
115 (!CM_IsEmpty(msg, eTimestamp) ? msg->cm_fields[eTimestamp] : "0"));
122 * cmd_msgs() - get list of message #'s in this room
123 * implements the MSGS server command using CtdlForEachMessage()
125 void cmd_msgs(char *cmdbuf)
133 int with_template = 0;
134 struct CtdlMessage *template = NULL;
136 char search_string[1024];
137 ForEachMsgCallback CallBack;
139 if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
141 extract_token(which, cmdbuf, 0, '|', sizeof which);
142 cm_ref = extract_int(cmdbuf, 1);
143 extract_token(search_string, cmdbuf, 1, '|', sizeof search_string);
144 with_template = extract_int(cmdbuf, 2);
145 switch (extract_int(cmdbuf, 3))
149 CallBack = simple_listing;
152 CallBack = headers_listing;
155 CallBack = headers_euid;
157 case MSG_HDRS_BRIEFFILTER:
159 CallBack = headers_brief_filter;
164 if (!strncasecmp(which, "OLD", 3))
166 else if (!strncasecmp(which, "NEW", 3))
168 else if (!strncasecmp(which, "FIRST", 5))
170 else if (!strncasecmp(which, "LAST", 4))
172 else if (!strncasecmp(which, "GT", 2))
174 else if (!strncasecmp(which, "LT", 2))
176 else if (!strncasecmp(which, "SEARCH", 6))
181 if ( (mode == MSGS_SEARCH) && (!CtdlGetConfigInt("c_enable_fulltext")) ) {
182 cprintf("%d Full text index is not enabled on this server.\n",
183 ERROR + CMD_NOT_SUPPORTED);
187 if (with_template == 1) {
190 cprintf("%d Send template then receive message list\n",
192 template = (struct CtdlMessage *)
193 malloc(sizeof(struct CtdlMessage));
194 memset(template, 0, sizeof(struct CtdlMessage));
195 template->cm_magic = CTDLMESSAGE_MAGIC;
196 template->cm_anon_type = MES_NORMAL;
198 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
202 tValueLen = extract_token(tfield, buf, 0, '|', sizeof tfield);
203 if ((tValueLen == 4) && GetFieldFromMnemonic(&f, tfield))
205 tValueLen = extract_token(tvalue, buf, 1, '|', sizeof tvalue);
206 if (tValueLen >= 0) {
207 CM_SetField(template, f, tvalue, tValueLen);
213 else if (with_template == 2) {
216 cprintf("%d Send list of headers\n",
218 filt.Filter = NewHash(1, lFlathash);
219 filt.buffer = NewStrBufPlain(NULL, 1024);
220 while(client_getln(buf, sizeof buf) >= 0 && strcmp(buf,"000")) {
223 if (GetFieldFromMnemonic(&f, buf))
225 Put(filt.Filter, LKEY(i), (void*)f, reference_free_handler);
229 filt.p = GetNewHashPos(filt.Filter, 0);
233 cprintf("%d \n", LISTING_FOLLOWS);
236 if (with_template < 2) {
237 CtdlForEachMessage(mode,
238 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
239 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
244 if (template != NULL) CM_Free(template);
247 CtdlForEachMessage(mode,
248 ( (mode == MSGS_SEARCH) ? 0 : cm_ref ),
249 ( (mode == MSGS_SEARCH) ? search_string : NULL ),
254 DeleteHashPos(&filt.p);
255 DeleteHash(&filt.Filter);
256 FreeStrBuf(&filt.buffer);
263 * display a message (mode 0 - Citadel proprietary)
265 void cmd_msg0(char *cmdbuf)
268 int headers_only = HEADERS_ALL;
270 msgid = extract_long(cmdbuf, 0);
271 headers_only = extract_int(cmdbuf, 1);
273 CtdlOutputMsg(msgid, MT_CITADEL, headers_only, 1, 0, NULL, 0, NULL, NULL, NULL);
279 * display a message (mode 2 - RFC822)
281 void cmd_msg2(char *cmdbuf)
284 int headers_only = HEADERS_ALL;
286 msgid = extract_long(cmdbuf, 0);
287 headers_only = extract_int(cmdbuf, 1);
289 CtdlOutputMsg(msgid, MT_RFC822, headers_only, 1, 1, NULL, 0, NULL, NULL, NULL);
295 * display a message (mode 3 - IGnet raw format - internal programs only)
297 void cmd_msg3(char *cmdbuf)
300 struct CtdlMessage *msg = NULL;
303 if (CC->internal_pgm == 0) {
304 cprintf("%d This command is for internal programs only.\n",
305 ERROR + HIGHER_ACCESS_REQUIRED);
309 msgnum = extract_long(cmdbuf, 0);
310 msg = CtdlFetchMessage(msgnum, 1, 1);
312 cprintf("%d Message %ld not found.\n",
313 ERROR + MESSAGE_NOT_FOUND, msgnum);
317 CtdlSerializeMessage(&smr, msg);
321 cprintf("%d Unable to serialize message\n",
322 ERROR + INTERNAL_ERROR);
326 cprintf("%d %ld\n", BINARY_FOLLOWS, (long)smr.len);
327 client_write((char *)smr.ser, (int)smr.len);
334 * Display a message using MIME content types
336 void cmd_msg4(char *cmdbuf)
341 msgid = extract_long(cmdbuf, 0);
342 extract_token(section, cmdbuf, 1, '|', sizeof section);
343 CtdlOutputMsg(msgid, MT_MIME, 0, 1, 0, (section[0] ? section : NULL) , 0, NULL, NULL, NULL);
349 * Client tells us its preferred message format(s)
351 void cmd_msgp(char *cmdbuf)
353 if (!strcasecmp(cmdbuf, "dont_decode")) {
354 CC->msg4_dont_decode = 1;
355 cprintf("%d MSG4 will not pre-decode messages.\n", CIT_OK);
358 safestrncpy(CC->preferred_formats, cmdbuf, sizeof(CC->preferred_formats));
359 cprintf("%d Preferred MIME formats have been set.\n", CIT_OK);
365 * Open a component of a MIME message as a download file
367 void cmd_opna(char *cmdbuf)
370 char desired_section[128];
372 msgid = extract_long(cmdbuf, 0);
373 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
374 safestrncpy(CC->download_desired_section, desired_section,
375 sizeof CC->download_desired_section);
376 CtdlOutputMsg(msgid, MT_DOWNLOAD, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
381 * Open a component of a MIME message and transmit it all at once
383 void cmd_dlat(char *cmdbuf)
386 char desired_section[128];
388 msgid = extract_long(cmdbuf, 0);
389 extract_token(desired_section, cmdbuf, 1, '|', sizeof desired_section);
390 safestrncpy(CC->download_desired_section, desired_section,
391 sizeof CC->download_desired_section);
392 CtdlOutputMsg(msgid, MT_SPEW_SECTION, 0, 1, 1, NULL, 0, NULL, NULL, NULL);
396 * message entry - mode 0 (normal)
398 void cmd_ent0(char *entargs)
400 struct CitContext *CCC = CC;
405 char supplied_euid[128];
408 char newusername[256];
409 char newuseremail[256];
410 struct CtdlMessage *msg;
414 recptypes *valid = NULL;
415 recptypes *valid_to = NULL;
416 recptypes *valid_cc = NULL;
417 recptypes *valid_bcc = NULL;
419 int subject_required = 0;
424 int newuseremail_ok = 0;
425 char references[SIZ];
430 post = extract_int(entargs, 0);
431 extract_token(recp, entargs, 1, '|', sizeof recp);
432 anon_flag = extract_int(entargs, 2);
433 format_type = extract_int(entargs, 3);
434 extract_token(subject, entargs, 4, '|', sizeof subject);
435 extract_token(newusername, entargs, 5, '|', sizeof newusername);
436 do_confirm = extract_int(entargs, 6);
437 extract_token(cc, entargs, 7, '|', sizeof cc);
438 extract_token(bcc, entargs, 8, '|', sizeof bcc);
439 switch(CC->room.QRdefaultview) {
443 extract_token(supplied_euid, entargs, 9, '|', sizeof supplied_euid);
446 supplied_euid[0] = 0;
449 extract_token(newuseremail, entargs, 10, '|', sizeof newuseremail);
450 extract_token(references, entargs, 11, '|', sizeof references);
451 for (ptr=references; *ptr != 0; ++ptr) {
452 if (*ptr == '!') *ptr = '|';
455 /* first check to make sure the request is valid. */
457 err = CtdlDoIHavePermissionToPostInThisRoom(
462 (!IsEmptyStr(references)) /* is this a reply? or a top-level post? */
466 cprintf("%d %s\n", err, errmsg);
470 /* Check some other permission type things. */
472 if (IsEmptyStr(newusername)) {
473 strcpy(newusername, CCC->user.fullname);
475 if ( (CCC->user.axlevel < AxAideU)
476 && (strcasecmp(newusername, CCC->user.fullname))
477 && (strcasecmp(newusername, CCC->cs_inet_fn))
479 cprintf("%d You don't have permission to author messages as '%s'.\n",
480 ERROR + HIGHER_ACCESS_REQUIRED,
487 if (IsEmptyStr(newuseremail)) {
491 if (!IsEmptyStr(newuseremail)) {
492 if (!strcasecmp(newuseremail, CCC->cs_inet_email)) {
495 else if (!IsEmptyStr(CCC->cs_inet_other_emails)) {
496 j = num_tokens(CCC->cs_inet_other_emails, '|');
497 for (i=0; i<j; ++i) {
498 extract_token(buf, CCC->cs_inet_other_emails, i, '|', sizeof buf);
499 if (!strcasecmp(newuseremail, buf)) {
506 if (!newuseremail_ok) {
507 cprintf("%d You don't have permission to author messages as '%s'.\n",
508 ERROR + HIGHER_ACCESS_REQUIRED,
514 CCC->cs_flags |= CS_POSTING;
516 /* In mailbox rooms we have to behave a little differently --
517 * make sure the user has specified at least one recipient. Then
518 * validate the recipient(s). We do this for the Mail> room, as
519 * well as any room which has the "Mailbox" view set - unless it
520 * is the DRAFTS room which does not require recipients
523 if ( ( ( (CCC->room.QRflags & QR_MAILBOX) && (!strcasecmp(&CCC->room.QRname[11], MAILROOM)) )
524 || ( (CCC->room.QRflags & QR_MAILBOX) && (CCC->curr_view == VIEW_MAILBOX) )
525 ) && (strcasecmp(&CCC->room.QRname[11], USERDRAFTROOM)) !=0 ) {
526 if (CCC->user.axlevel < AxProbU) {
527 strcpy(recp, "sysop");
532 valid_to = validate_recipients(recp, NULL, 0);
533 if (valid_to->num_error > 0) {
534 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_to->errormsg);
535 free_recipients(valid_to);
539 valid_cc = validate_recipients(cc, NULL, 0);
540 if (valid_cc->num_error > 0) {
541 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_cc->errormsg);
542 free_recipients(valid_to);
543 free_recipients(valid_cc);
547 valid_bcc = validate_recipients(bcc, NULL, 0);
548 if (valid_bcc->num_error > 0) {
549 cprintf("%d %s\n", ERROR + NO_SUCH_USER, valid_bcc->errormsg);
550 free_recipients(valid_to);
551 free_recipients(valid_cc);
552 free_recipients(valid_bcc);
556 /* Recipient required, but none were specified */
557 if ( (valid_to->num_error < 0) && (valid_cc->num_error < 0) && (valid_bcc->num_error < 0) ) {
558 free_recipients(valid_to);
559 free_recipients(valid_cc);
560 free_recipients(valid_bcc);
561 cprintf("%d At least one recipient is required.\n", ERROR + NO_SUCH_USER);
565 if (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0) {
566 if (CtdlCheckInternetMailPermission(&CCC->user)==0) {
567 cprintf("%d You do not have permission "
568 "to send Internet mail.\n",
569 ERROR + HIGHER_ACCESS_REQUIRED);
570 free_recipients(valid_to);
571 free_recipients(valid_cc);
572 free_recipients(valid_bcc);
577 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)
578 && (CCC->user.axlevel < AxNetU) ) {
579 cprintf("%d Higher access required for network mail.\n",
580 ERROR + HIGHER_ACCESS_REQUIRED);
581 free_recipients(valid_to);
582 free_recipients(valid_cc);
583 free_recipients(valid_bcc);
587 if ((RESTRICT_INTERNET == 1)
588 && (valid_to->num_internet + valid_cc->num_internet + valid_bcc->num_internet > 0)
589 && ((CCC->user.flags & US_INTERNET) == 0)
590 && (!CCC->internal_pgm)) {
591 cprintf("%d You don't have access to Internet mail.\n",
592 ERROR + HIGHER_ACCESS_REQUIRED);
593 free_recipients(valid_to);
594 free_recipients(valid_cc);
595 free_recipients(valid_bcc);
601 /* Is this a room which has anonymous-only or anonymous-option? */
602 anonymous = MES_NORMAL;
603 if (CCC->room.QRflags & QR_ANONONLY) {
604 anonymous = MES_ANONONLY;
606 if (CCC->room.QRflags & QR_ANONOPT) {
607 if (anon_flag == 1) { /* only if the user requested it */
608 anonymous = MES_ANONOPT;
612 if ((CCC->room.QRflags & QR_MAILBOX) == 0) {
616 /* Recommend to the client that the use of a message subject is
617 * strongly recommended in this room, if either the SUBJECTREQ flag
618 * is set, or if there is one or more Internet email recipients.
620 if (CCC->room.QRflags2 & QR2_SUBJECTREQ) subject_required = 1;
621 if ((valid_to) && (valid_to->num_internet > 0)) subject_required = 1;
622 if ((valid_cc) && (valid_cc->num_internet > 0)) subject_required = 1;
623 if ((valid_bcc) && (valid_bcc->num_internet > 0)) subject_required = 1;
625 /* If we're only checking the validity of the request, return
626 * success without creating the message.
629 cprintf("%d %s|%d\n", CIT_OK,
630 ((valid_to != NULL) ? valid_to->display_recp : ""),
632 free_recipients(valid_to);
633 free_recipients(valid_cc);
634 free_recipients(valid_bcc);
638 /* We don't need these anymore because we'll do it differently below */
639 free_recipients(valid_to);
640 free_recipients(valid_cc);
641 free_recipients(valid_bcc);
643 /* Read in the message from the client. */
645 cprintf("%d send message\n", START_CHAT_MODE);
647 cprintf("%d send message\n", SEND_LISTING);
650 msg = CtdlMakeMessage(&CCC->user, recp, cc,
651 CCC->room.QRname, anonymous, format_type,
652 newusername, newuseremail, subject,
653 ((!IsEmptyStr(supplied_euid)) ? supplied_euid : NULL),
656 /* Put together one big recipients struct containing to/cc/bcc all in
657 * one. This is for the envelope.
659 char *all_recps = malloc(SIZ * 3);
660 strcpy(all_recps, recp);
661 if (!IsEmptyStr(cc)) {
662 if (!IsEmptyStr(all_recps)) {
663 strcat(all_recps, ",");
665 strcat(all_recps, cc);
667 if (!IsEmptyStr(bcc)) {
668 if (!IsEmptyStr(all_recps)) {
669 strcat(all_recps, ",");
671 strcat(all_recps, bcc);
673 if (!IsEmptyStr(all_recps)) {
674 valid = validate_recipients(all_recps, NULL, 0);
681 if ((valid != NULL) && (valid->num_room == 1) && !IsEmptyStr(valid->recp_orgroom))
683 /* posting into an ML room? set the envelope from
684 * to the actual mail address so others get a valid
687 CM_SetField(msg, eenVelopeTo, valid->recp_orgroom, strlen(valid->recp_orgroom));
691 msgnum = CtdlSubmitMsg(msg, valid, "", QP_EADDR);
693 cprintf("%ld\n", msgnum);
695 if (StrLength(CCC->StatusMessage) > 0) {
696 cprintf("%s\n", ChrPtr(CCC->StatusMessage));
698 else if (msgnum >= 0L) {
699 client_write(HKEY("Message accepted.\n"));
702 client_write(HKEY("Internal error.\n"));
705 if (!CM_IsEmpty(msg, eExclusiveID)) {
706 cprintf("%s\n", msg->cm_fields[eExclusiveID]);
716 free_recipients(valid);
722 * Delete message from current room
724 void cmd_dele(char *args)
733 extract_token(msgset, args, 0, '|', sizeof msgset);
734 num_msgs = num_tokens(msgset, ',');
736 cprintf("%d Nothing to do.\n", CIT_OK);
740 if (CtdlDoIHavePermissionToDeleteMessagesFromThisRoom() == 0) {
741 cprintf("%d Higher access required.\n",
742 ERROR + HIGHER_ACCESS_REQUIRED);
747 * Build our message set to be moved/copied
749 msgs = malloc(num_msgs * sizeof(long));
750 for (i=0; i<num_msgs; ++i) {
751 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
752 msgs[i] = atol(msgtok);
755 num_deleted = CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
759 cprintf("%d %d message%s deleted.\n", CIT_OK,
760 num_deleted, ((num_deleted != 1) ? "s" : ""));
762 cprintf("%d Message not found.\n", ERROR + MESSAGE_NOT_FOUND);
769 * move or copy a message to another room
771 void cmd_move(char *args)
778 char targ[ROOMNAMELEN];
779 struct ctdlroom qtemp;
786 extract_token(msgset, args, 0, '|', sizeof msgset);
787 num_msgs = num_tokens(msgset, ',');
789 cprintf("%d Nothing to do.\n", CIT_OK);
793 extract_token(targ, args, 1, '|', sizeof targ);
794 convert_room_name_macros(targ, sizeof targ);
795 targ[ROOMNAMELEN - 1] = 0;
796 is_copy = extract_int(args, 2);
798 if (CtdlGetRoom(&qtemp, targ) != 0) {
799 cprintf("%d '%s' does not exist.\n", ERROR + ROOM_NOT_FOUND, targ);
803 if (!strcasecmp(qtemp.QRname, CC->room.QRname)) {
804 cprintf("%d Source and target rooms are the same.\n", ERROR + ALREADY_EXISTS);
808 CtdlGetUser(&CC->user, CC->curr_user);
809 CtdlRoomAccess(&qtemp, &CC->user, &ra, NULL);
811 /* Check for permission to perform this operation.
812 * Remember: "CC->room" is source, "qtemp" is target.
816 /* Admins can move/copy */
817 if (CC->user.axlevel >= AxAideU) permit = 1;
819 /* Room aides can move/copy */
820 if (CC->user.usernum == CC->room.QRroomaide) permit = 1;
822 /* Permit move/copy from personal rooms */
823 if ((CC->room.QRflags & QR_MAILBOX)
824 && (qtemp.QRflags & QR_MAILBOX)) permit = 1;
826 /* Permit only copy from public to personal room */
828 && (!(CC->room.QRflags & QR_MAILBOX))
829 && (qtemp.QRflags & QR_MAILBOX)) permit = 1;
831 /* Permit message removal from collaborative delete rooms */
832 if (CC->room.QRflags2 & QR2_COLLABDEL) permit = 1;
834 /* Users allowed to post into the target room may move into it too. */
835 if ((CC->room.QRflags & QR_MAILBOX) &&
836 (qtemp.QRflags & UA_POSTALLOWED)) permit = 1;
838 /* User must have access to target room */
839 if (!(ra & UA_KNOWN)) permit = 0;
842 cprintf("%d Higher access required.\n",
843 ERROR + HIGHER_ACCESS_REQUIRED);
848 * Build our message set to be moved/copied
850 msgs = malloc(num_msgs * sizeof(long));
851 for (i=0; i<num_msgs; ++i) {
852 extract_token(msgtok, msgset, i, ',', sizeof msgtok);
853 msgs[i] = atol(msgtok);
859 err = CtdlSaveMsgPointersInRoom(targ, msgs, num_msgs, 1, NULL, 0);
861 cprintf("%d Cannot store message(s) in %s: error %d\n",
867 /* Now delete the message from the source room,
868 * if this is a 'move' rather than a 'copy' operation.
871 CtdlDeleteMessages(CC->room.QRname, msgs, num_msgs, "");
875 cprintf("%d Message(s) %s.\n", CIT_OK, (is_copy ? "copied" : "moved") );
879 /*****************************************************************************/
880 /* MODULE INITIALIZATION STUFF */
881 /*****************************************************************************/
882 CTDL_MODULE_INIT(ctdl_message)
886 CtdlRegisterProtoHook(cmd_msgs, "MSGS", "Output a list of messages in the current room");
887 CtdlRegisterProtoHook(cmd_msg0, "MSG0", "Output a message in plain text format");
888 CtdlRegisterProtoHook(cmd_msg2, "MSG2", "Output a message in RFC822 format");
889 CtdlRegisterProtoHook(cmd_msg3, "MSG3", "Output a message in raw format (deprecated)");
890 CtdlRegisterProtoHook(cmd_msg4, "MSG4", "Output a message in the client's preferred format");
891 CtdlRegisterProtoHook(cmd_msgp, "MSGP", "Select preferred format for MSG4 output");
892 CtdlRegisterProtoHook(cmd_opna, "OPNA", "Open an attachment for download");
893 CtdlRegisterProtoHook(cmd_dlat, "DLAT", "Download an attachment");
894 CtdlRegisterProtoHook(cmd_ent0, "ENT0", "Enter a message");
895 CtdlRegisterProtoHook(cmd_dele, "DELE", "Delete a message");
896 CtdlRegisterProtoHook(cmd_move, "MOVE", "Move or copy a message to another room");
899 /* return our Subversion id for the Log */
900 return "ctdl_message";