Rename StrBufRFC2047encodeMessage() to StrBufQuotedPrintableEncode()
[citadel.git] / citadel / server / modules / instmsg / serv_instmsg.c
1 /*
2  * This module handles instant messaging between users.
3  * 
4  * Copyright (c) 1987-2022 by the citadel.org team
5  *
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.
8  *
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.
13  */
14
15 #include <stdlib.h>
16 #include <unistd.h>
17 #include <stdio.h>
18 #include <fcntl.h>
19 #include <signal.h>
20 #include <pwd.h>
21 #include <errno.h>
22 #include <sys/types.h>
23 #include <time.h>
24 #include <sys/wait.h>
25 #include <string.h>
26 #include <limits.h>
27 #include <libcitadel.h>
28 #include "../../sysdep.h"
29 #include "../../citadel.h"
30 #include "../../server.h"
31 #include "../../context.h"
32 #include "../../citserver.h"
33 #include "../../support.h"
34 #include "../../config.h"
35 #include "../../msgbase.h"
36 #include "../../user_ops.h"
37 #include "../../ctdl_module.h"
38 #include "serv_instmsg.h"
39
40 struct imlog {
41         struct imlog *next;
42         long usernums[2];
43         char usernames[2][128];
44         time_t lastmsg;
45         int last_serial;
46         StrBuf *conversation;
47 };
48
49 struct imlog *imlist = NULL;
50
51 /*
52  * This function handles the logging of instant messages to disk.
53  */
54 void log_instant_message(struct CitContext *me, struct CitContext *them, char *msgtext, int serial_number)
55 {
56         long usernums[2];
57         long t;
58         struct imlog *iptr = NULL;
59         struct imlog *this_im = NULL;
60         
61         memset(usernums, 0, sizeof usernums);
62         usernums[0] = me->user.usernum;
63         usernums[1] = them->user.usernum;
64
65         /* Always put the lower user number first, so we can use the array as a hash value which
66          * represents a pair of users.  For a broadcast message one of the users will be 0.
67          */
68         if (usernums[0] > usernums[1]) {
69                 t = usernums[0];
70                 usernums[0] = usernums[1];
71                 usernums[1] = t;
72         }
73
74         begin_critical_section(S_IM_LOGS);
75
76         /* Look for an existing conversation in the hash table.
77          * If not found, create a new one.
78          */
79
80         this_im = NULL;
81         for (iptr = imlist; iptr != NULL; iptr = iptr->next) {
82                 if ((iptr->usernums[0] == usernums[0]) && (iptr->usernums[1] == usernums[1])) {
83                         /* Existing conversation */
84                         this_im = iptr;
85                 }
86         }
87         if (this_im == NULL) {
88                 /* New conversation */
89                 this_im = malloc(sizeof(struct imlog));
90                 memset(this_im, 0, sizeof (struct imlog));
91                 this_im->usernums[0] = usernums[0];
92                 this_im->usernums[1] = usernums[1];
93                 /* usernames[] and usernums[] might not be in the same order.  This is not an error. */
94                 if (me) {
95                         safestrncpy(this_im->usernames[0], me->user.fullname, sizeof this_im->usernames[0]);
96                 }
97                 if (them) {
98                         safestrncpy(this_im->usernames[1], them->user.fullname, sizeof this_im->usernames[1]);
99                 }
100                 this_im->conversation = NewStrBuf();
101                 this_im->next = imlist;
102                 imlist = this_im;
103                 StrBufAppendBufPlain(this_im->conversation, HKEY("<html><body>\r\n"), 0);
104         }
105
106         /* Since it's possible for this function to get called more than once if a user is logged
107          * in on multiple sessions, we use the message's serial number to keep track of whether
108          * we've already logged it.
109          */
110         if (this_im->last_serial != serial_number)
111         {
112                 this_im->lastmsg = time(NULL);          /* Touch the timestamp so we know when to flush */
113                 this_im->last_serial = serial_number;
114                 StrBufAppendBufPlain(this_im->conversation, HKEY("<p><b>"), 0);
115                 StrBufAppendBufPlain(this_im->conversation, me->user.fullname, -1, 0);
116                 StrBufAppendBufPlain(this_im->conversation, HKEY(":</b> "), 0);
117                 StrEscAppend(this_im->conversation, NULL, msgtext, 0, 0);
118                 StrBufAppendBufPlain(this_im->conversation, HKEY("</p>\r\n"), 0);
119         }
120         end_critical_section(S_IM_LOGS);
121 }
122
123
124 /*
125  * Delete any remaining instant messages
126  */
127 void delete_instant_messages(void) {
128         struct ExpressMessage *ptr;
129
130         begin_critical_section(S_SESSION_TABLE);
131         while (CC->FirstExpressMessage != NULL) {
132                 ptr = CC->FirstExpressMessage->next;
133                 if (CC->FirstExpressMessage->text != NULL)
134                         free(CC->FirstExpressMessage->text);
135                 free(CC->FirstExpressMessage);
136                 CC->FirstExpressMessage = ptr;
137         }
138         end_critical_section(S_SESSION_TABLE);
139 }
140
141
142 /*
143  * Retrieve instant messages
144  */
145 void cmd_gexp(char *argbuf) {
146         struct ExpressMessage *ptr;
147
148         if (CC->FirstExpressMessage == NULL) {
149                 cprintf("%d No instant messages waiting.\n", ERROR + MESSAGE_NOT_FOUND);
150                 return;
151         }
152
153         begin_critical_section(S_SESSION_TABLE);
154         ptr = CC->FirstExpressMessage;
155         CC->FirstExpressMessage = CC->FirstExpressMessage->next;
156         end_critical_section(S_SESSION_TABLE);
157
158         cprintf("%d %d|%ld|%d|%s|%s|%s\n",
159                 LISTING_FOLLOWS,
160                 ((ptr->next != NULL) ? 1 : 0),          /* more msgs? */
161                 (long)ptr->timestamp,                   /* time sent */
162                 ptr->flags,                             /* flags */
163                 ptr->sender,                            /* sender of msg */
164                 CtdlGetConfigStr("c_nodename"),         /* static for now (and possibly deprecated) */
165                 ptr->sender_email                       /* email or jid of sender */
166         );
167
168         if (ptr->text != NULL) {
169                 memfmout(ptr->text, "\n");
170                 free(ptr->text);
171         }
172
173         cprintf("000\n");
174         free(ptr);
175 }
176
177
178 /*
179  * Asynchronously deliver instant messages
180  */
181 void cmd_gexp_async(void) {
182
183         /* Only do this if the session can handle asynchronous protocol */
184         if (CC->is_async == 0) return;
185
186         /* And don't do it if there's nothing to send. */
187         if (CC->FirstExpressMessage == NULL) return;
188
189         cprintf("%d instant msg\n", ASYNC_MSG + ASYNC_GEXP);
190 }
191
192
193 /*
194  * Back end support function for send_instant_message() and company
195  */
196 void add_xmsg_to_context(struct CitContext *ccptr, struct ExpressMessage *newmsg) 
197 {
198         struct ExpressMessage *findend;
199
200         if (ccptr->FirstExpressMessage == NULL) {
201                 ccptr->FirstExpressMessage = newmsg;
202         }
203         else {
204                 findend = ccptr->FirstExpressMessage;
205                 while (findend->next != NULL) {
206                         findend = findend->next;
207                 }
208                 findend->next = newmsg;
209         }
210
211         /* If the target context is a session which can handle asynchronous
212          * messages, go ahead and set the flag for that.
213          */
214         set_async_waiting(ccptr);
215 }
216
217
218 /* 
219  * This is the back end to the instant message sending function.  
220  * Returns the number of users to which the message was sent.
221  * Sending a zero-length message tests for recipients without sending messages.
222  */
223 int send_instant_message(char *lun, char *lem, char *x_user, char *x_msg)
224 {
225         int message_sent = 0;           /* number of successful sends */
226         struct CitContext *ccptr;
227         struct ExpressMessage *newmsg = NULL;
228         int do_send = 0;                /* 1 = send message; 0 = only check for valid recipient */
229         static int serial_number = 0;   /* this keeps messages from getting logged twice */
230
231         if (!IsEmptyStr(x_msg)) {
232                 do_send = 1;
233         }
234
235         /* find the target user's context and append the message */
236         begin_critical_section(S_SESSION_TABLE);
237         ++serial_number;
238         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
239
240                 if ( ((!strcasecmp(ccptr->user.fullname, x_user))
241                     || (!strcasecmp(x_user, "broadcast")))
242                     && (ccptr->can_receive_im)
243                     && ((ccptr->disable_exp == 0)
244                     || (CC->user.axlevel >= AxAideU)) ) {
245                         if (do_send) {
246                                 newmsg = (struct ExpressMessage *) malloc(sizeof (struct ExpressMessage));
247                                 memset(newmsg, 0, sizeof (struct ExpressMessage));
248                                 time(&(newmsg->timestamp));
249                                 safestrncpy(newmsg->sender, lun, sizeof newmsg->sender);
250                                 safestrncpy(newmsg->sender_email, lem, sizeof newmsg->sender_email);
251                                 if (!strcasecmp(x_user, "broadcast")) {
252                                         newmsg->flags |= EM_BROADCAST;
253                                 }
254                                 newmsg->text = strdup(x_msg);
255
256                                 add_xmsg_to_context(ccptr, newmsg);
257
258                                 /* and log it ... */
259                                 if (ccptr != CC) {
260                                         log_instant_message(CC, ccptr, newmsg->text, serial_number);
261                                 }
262                         }
263                         ++message_sent;
264                 }
265         }
266         end_critical_section(S_SESSION_TABLE);
267         return (message_sent);
268 }
269
270
271 /*
272  * send instant messages
273  */
274 void cmd_sexp(char *argbuf)
275 {
276         int message_sent = 0;
277         char x_user[USERNAME_SIZE];
278         char x_msg[1024];
279         char *lem;
280         char *x_big_msgbuf = NULL;
281
282         if ((!(CC->logged_in)) && (!(CC->internal_pgm))) {
283                 cprintf("%d Not logged in.\n", ERROR + NOT_LOGGED_IN);
284                 return;
285         }
286
287         lem = CC->cs_principal_id;
288
289         extract_token(x_user, argbuf, 0, '|', sizeof x_user);
290         extract_token(x_msg, argbuf, 1, '|', sizeof x_msg);
291
292         if (!x_user[0]) {
293                 cprintf("%d You were not previously paged.\n", ERROR + NO_SUCH_USER);
294                 return;
295         }
296         if ((!strcasecmp(x_user, "broadcast")) && (CC->user.axlevel < AxAideU)) {
297                 cprintf("%d Higher access required to send a broadcast.\n",
298                         ERROR + HIGHER_ACCESS_REQUIRED);
299                 return;
300         }
301         /* This loop handles text-transfer pages */
302         if (!strcmp(x_msg, "-")) {
303                 message_sent = PerformXmsgHooks(CC->user.fullname, lem, x_user, "");
304                 if (message_sent == 0) {
305                         if (CtdlGetUser(NULL, x_user))
306                                 cprintf("%d '%s' does not exist.\n",
307                                                 ERROR + NO_SUCH_USER, x_user);
308                         else
309                                 cprintf("%d '%s' is not logged in "
310                                                 "or is not accepting pages.\n",
311                                                 ERROR + RESOURCE_NOT_OPEN, x_user);
312                         return;
313                 }
314                 unbuffer_output();
315                 cprintf("%d Transmit message (will deliver to %d users)\n",
316                         SEND_LISTING, message_sent);
317                 x_big_msgbuf = malloc(SIZ);
318                 memset(x_big_msgbuf, 0, SIZ);
319                 while (client_getln(x_msg, sizeof x_msg) >= 0 && strcmp(x_msg, "000")) {
320                         x_big_msgbuf = realloc(x_big_msgbuf,
321                                strlen(x_big_msgbuf) + strlen(x_msg) + 4);
322                         if (!IsEmptyStr(x_big_msgbuf))
323                            if (x_big_msgbuf[strlen(x_big_msgbuf)] != '\n')
324                                 strcat(x_big_msgbuf, "\n");
325                         strcat(x_big_msgbuf, x_msg);
326                 }
327                 PerformXmsgHooks(CC->user.fullname, lem, x_user, x_big_msgbuf);
328                 free(x_big_msgbuf);
329
330                 /* This loop handles inline pages */
331         } else {
332                 message_sent = PerformXmsgHooks(CC->user.fullname, lem, x_user, x_msg);
333
334                 if (message_sent > 0) {
335                         if (!IsEmptyStr(x_msg)) {
336                                 cprintf("%d Message sent", CIT_OK);
337                         }
338                         else {
339                                 cprintf("%d Ok to send message", CIT_OK);
340                         }
341                         if (message_sent > 1) {
342                                 cprintf(" to %d users", message_sent);
343                         }
344                         cprintf(".\n");
345                 } else {
346                         if (CtdlGetUser(NULL, x_user)) {
347                                 cprintf("%d '%s' does not exist.\n", ERROR + NO_SUCH_USER, x_user);
348                         }
349                         else {
350                                 cprintf("%d '%s' is not logged in or is not accepting instant messages.\n",
351                                                 ERROR + RESOURCE_NOT_OPEN, x_user);
352                         }
353                 }
354
355
356         }
357 }
358
359
360 /*
361  * Enter or exit paging-disabled mode
362  */
363 void cmd_dexp(char *argbuf)
364 {
365         int new_state;
366
367         if (CtdlAccessCheck(ac_logged_in)) return;
368
369         new_state = extract_int(argbuf, 0);
370         if ((new_state == 0) || (new_state == 1)) {
371                 CC->disable_exp = new_state;
372         }
373
374         cprintf("%d %d\n", CIT_OK, CC->disable_exp);
375 }
376
377
378 /*
379  * Request client termination
380  */
381 void cmd_reqt(char *argbuf) {
382         struct CitContext *ccptr;
383         int sessions = 0;
384         int which_session;
385         struct ExpressMessage *newmsg;
386
387         if (CtdlAccessCheck(ac_aide)) return;
388         which_session = extract_int(argbuf, 0);
389
390         begin_critical_section(S_SESSION_TABLE);
391         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
392                 if ((ccptr->cs_pid == which_session) || (which_session == 0)) {
393
394                         newmsg = (struct ExpressMessage *)
395                                 malloc(sizeof (struct ExpressMessage));
396                         memset(newmsg, 0,
397                                 sizeof (struct ExpressMessage));
398                         time(&(newmsg->timestamp));
399                         safestrncpy(newmsg->sender, CC->user.fullname,
400                                     sizeof newmsg->sender);
401                         newmsg->flags |= EM_GO_AWAY;
402                         newmsg->text = strdup("Automatic logoff requested.");
403
404                         add_xmsg_to_context(ccptr, newmsg);
405                         ++sessions;
406
407                 }
408         }
409         end_critical_section(S_SESSION_TABLE);
410         cprintf("%d Sent termination request to %d sessions.\n", CIT_OK, sessions);
411 }
412
413
414 /*
415  * This is the back end for flush_conversations_to_disk()
416  * At this point we've isolated a single conversation (struct imlog)
417  * and are ready to write it to disk.
418  */
419 void flush_individual_conversation(struct imlog *im) {
420         struct CtdlMessage *msg;
421         long msgnum = 0;
422         char roomname[ROOMNAMELEN];
423         StrBuf *MsgBuf, *FullMsgBuf;
424
425         StrBufAppendBufPlain(im->conversation, HKEY(
426                 "</body>\r\n"
427                 "</html>\r\n"
428                 ), 0
429         );
430
431         MsgBuf = StrBufQuotedPrintableEncode(im->conversation);
432         FlushStrBuf(im->conversation);
433         FullMsgBuf = NewStrBufPlain(NULL, StrLength(im->conversation) + 100);
434
435         StrBufAppendBufPlain(FullMsgBuf, HKEY(
436                 "Content-type: text/html; charset=UTF-8\r\n"
437                 "Content-Transfer-Encoding: quoted-printable\r\n"
438                 "\r\n"
439                 ), 0
440         );
441         StrBufAppendBuf (FullMsgBuf, MsgBuf, 0);
442         FreeStrBuf(&MsgBuf);
443
444         msg = malloc(sizeof(struct CtdlMessage));
445         memset(msg, 0, sizeof(struct CtdlMessage));
446         msg->cm_magic = CTDLMESSAGE_MAGIC;
447         msg->cm_anon_type = MES_NORMAL;
448         msg->cm_format_type = FMT_RFC822;
449         if (!IsEmptyStr(im->usernames[0])) {
450                 CM_SetField(msg, eAuthor, im->usernames[0], strlen(im->usernames[0]));
451         } else {
452                 CM_SetField(msg, eAuthor, HKEY("Citadel"));
453         }
454         if (!IsEmptyStr(im->usernames[1])) {
455                 CM_SetField(msg, eRecipient, im->usernames[1], strlen(im->usernames[1]));
456         }
457
458         CM_SetField(msg, eOriginalRoom, HKEY(PAGELOGROOM));
459         CM_SetAsFieldSB(msg, eMesageText, &FullMsgBuf); /* we own this memory now */
460
461         /* Start with usernums[1] because it's guaranteed to be higher than usernums[0],
462          * so if there's only one party, usernums[0] will be zero but usernums[1] won't.
463          * Create the room if necessary.  Note that we create as a type 5 room rather
464          * than 4, which indicates that it's a personal room but we've already supplied
465          * the namespace prefix.
466          *
467          * In the unlikely event that usernums[1] is zero, a room with an invalid namespace
468          * prefix will be created.  That's ok because the auto-purger will clean it up later.
469          */
470         snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[1], PAGELOGROOM);
471         CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
472         msgnum = CtdlSubmitMsg(msg, NULL, roomname);
473         CM_Free(msg);
474
475         /* If there is a valid user number in usernums[0], save a copy for them too. */
476         if (im->usernums[0] > 0) {
477                 snprintf(roomname, sizeof roomname, "%010ld.%s", im->usernums[0], PAGELOGROOM);
478                 CtdlCreateRoom(roomname, 5, "", 0, 1, 1, VIEW_BBS);
479                 CtdlSaveMsgPointerInRoom(roomname, msgnum, 0, NULL);
480         }
481
482         /* Finally, if we're logging instant messages globally, do that now. */
483         if (!IsEmptyStr(CtdlGetConfigStr("c_logpages"))) {
484                 CtdlCreateRoom(CtdlGetConfigStr("c_logpages"), 3, "", 0, 1, 1, VIEW_BBS);
485                 CtdlSaveMsgPointerInRoom(CtdlGetConfigStr("c_logpages"), msgnum, 0, NULL);
486         }
487
488 }
489
490 /*
491  * Locate instant message conversations which have gone idle
492  * (or, if the server is shutting down, locate *all* conversations)
493  * and flush them to disk (in the participants' log rooms, etc.)
494  */
495 void flush_conversations_to_disk(time_t if_older_than) {
496
497         struct imlog *flush_these = NULL;
498         struct imlog *dont_flush_these = NULL;
499         struct imlog *imptr = NULL;
500         struct CitContext *nptr;
501         int nContexts, i;
502
503         nptr = CtdlGetContextArray(&nContexts) ;        /* Make a copy of the current wholist */
504
505         begin_critical_section(S_IM_LOGS);
506         while (imlist)
507         {
508                 imptr = imlist;
509                 imlist = imlist->next;
510
511                 /* For a two party conversation, if one party has logged out, force flush. */
512                 if (nptr) {
513                         int user0_is_still_online = 0;
514                         int user1_is_still_online = 0;
515                         for (i=0; i<nContexts; i++)  {
516                                 if (nptr[i].user.usernum == imptr->usernums[0]) ++user0_is_still_online;
517                                 if (nptr[i].user.usernum == imptr->usernums[1]) ++user1_is_still_online;
518                         }
519                         if (imptr->usernums[0] != imptr->usernums[1]) {         /* two party conversation */
520                                 if ((!user0_is_still_online) || (!user1_is_still_online)) {
521                                         imptr->lastmsg = 0L;    /* force flush */
522                                 }
523                         }
524                         else {          /* one party conversation (yes, people do IM themselves) */
525                                 if (!user0_is_still_online) {
526                                         imptr->lastmsg = 0L;    /* force flush */
527                                 }
528                         }
529                 }
530
531                 /* Now test this conversation to see if it qualifies for flushing. */
532                 if ((time(NULL) - imptr->lastmsg) > if_older_than)
533                 {
534                         /* This conversation qualifies.  Move it to the list of ones to flush. */
535                         imptr->next = flush_these;
536                         flush_these = imptr;
537                 }
538                 else  {
539                         /* Move it to the list of ones not to flush. */
540                         imptr->next = dont_flush_these;
541                         dont_flush_these = imptr;
542                 }
543         }
544         imlist = dont_flush_these;
545         end_critical_section(S_IM_LOGS);
546         free(nptr);
547
548         /* We are now outside of the critical section, and we are the only thread holding a
549          * pointer to a linked list of conversations to be flushed to disk.
550          */
551         while (flush_these) {
552
553                 flush_individual_conversation(flush_these);     /* This will free the string buffer */
554                 imptr = flush_these;
555                 flush_these = flush_these->next;
556                 free(imptr);
557         }
558 }
559
560
561 void instmsg_timer(void) {
562         flush_conversations_to_disk(300);       /* Anything that hasn't peeped in more than 5 minutes */
563 }
564
565
566 void instmsg_shutdown(void) {
567         flush_conversations_to_disk(0);         /* Get it ALL onto disk NOW. */
568 }
569
570
571 // Initialization function, called from modules_init.c
572 char *ctdl_module_init_instmsg(void) {
573         if (!threading) {
574                 CtdlRegisterProtoHook(cmd_gexp, "GEXP", "Get instant messages");
575                 CtdlRegisterProtoHook(cmd_sexp, "SEXP", "Send an instant message");
576                 CtdlRegisterProtoHook(cmd_dexp, "DEXP", "Disable instant messages");
577                 CtdlRegisterProtoHook(cmd_reqt, "REQT", "Request client termination");
578                 CtdlRegisterSessionHook(cmd_gexp_async, EVT_ASYNC, PRIO_ASYNC + 1);
579                 CtdlRegisterSessionHook(delete_instant_messages, EVT_STOP, PRIO_STOP + 1);
580                 CtdlRegisterXmsgHook(send_instant_message, XMSG_PRI_LOCAL);
581                 CtdlRegisterSessionHook(instmsg_timer, EVT_TIMER, PRIO_CLEANUP + 400);
582                 CtdlRegisterSessionHook(instmsg_shutdown, EVT_SHUTDOWN, PRIO_SHUTDOWN + 10);
583         }
584         
585         /* return our module name for the log */
586         return "instmsg";
587 }