shift around the order of the functions because we're going to remove some duplicated...
[citadel.git] / citadel / modules / inboxrules / serv_inboxrules.c
1 /*
2  * Inbox handling rules
3  *
4  * Copyright (c) 1987-2020 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 "sysdep.h"
16 #include <stdlib.h>
17 #include <unistd.h>
18 #include <stdio.h>
19 #include <fcntl.h>
20 #include <ctype.h>
21 #include <pwd.h>
22 #include <errno.h>
23 #include <sys/types.h>
24
25 #if TIME_WITH_SYS_TIME
26 # include <sys/time.h>
27 # include <time.h>
28 #else
29 # if HAVE_SYS_TIME_H
30 #  include <sys/time.h>
31 # else
32 #  include <time.h>
33 # endif
34 #endif
35
36 #include <sys/wait.h>
37 #include <string.h>
38 #include <limits.h>
39 #include <libcitadel.h>
40 #include "citadel.h"
41 #include "server.h"
42 #include "citserver.h"
43 #include "support.h"
44 #include "config.h"
45 #include "database.h"
46 #include "msgbase.h"
47 #include "internet_addressing.h"
48 #include "ctdl_module.h"
49
50
51 #if 0
52
53 /*
54  * Callback function to redirect a message to a different folder
55  */
56 int ctdl_redirect(void)
57 {
58         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
59         struct CtdlMessage *msg = NULL;
60         recptypes *valid = NULL;
61         char recp[256];
62
63         safestrncpy(recp, sieve2_getvalue_string(s, "address"), sizeof recp);
64
65         syslog(LOG_DEBUG, "Action is REDIRECT, recipient <%s>", recp);
66
67         valid = validate_recipients(recp, NULL, 0);
68         if (valid == NULL) {
69                 syslog(LOG_WARNING, "REDIRECT failed: bad recipient <%s>", recp);
70                 return SIEVE2_ERROR_BADARGS;
71         }
72         if (valid->num_error > 0) {
73                 syslog(LOG_WARNING, "REDIRECT failed: bad recipient <%s>", recp);
74                 free_recipients(valid);
75                 return SIEVE2_ERROR_BADARGS;
76         }
77
78         msg = CtdlFetchMessage(cs->msgnum, 1, 1);
79         if (msg == NULL) {
80                 syslog(LOG_WARNING, "REDIRECT failed: unable to fetch msg %ld", cs->msgnum);
81                 free_recipients(valid);
82                 return SIEVE2_ERROR_BADARGS;
83         }
84
85         CtdlSubmitMsg(msg, valid, NULL, 0);
86         cs->cancel_implicit_keep = 1;
87         free_recipients(valid);
88         CM_Free(msg);
89         return SIEVE2_OK;
90 }
91
92
93 /*
94  * Callback function to indicate that a message *will* be kept in the inbox
95  */
96 int ctdl_keep(sieve2_context_t *s, void *my)
97 {
98         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
99         
100         syslog(LOG_DEBUG, "Action is KEEP");
101
102         cs->keep = 1;
103         cs->cancel_implicit_keep = 1;
104         return SIEVE2_OK;
105 }
106
107
108 /*
109  * Callback function to file a message into a different mailbox
110  */
111 int ctdl_fileinto(sieve2_context_t *s, void *my)
112 {
113         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
114         const char *dest_folder = sieve2_getvalue_string(s, "mailbox");
115         int c;
116         char foldername[256];
117         char original_room_name[ROOMNAMELEN];
118
119         syslog(LOG_DEBUG, "Action is FILEINTO, destination is <%s>", dest_folder);
120
121         /* FILEINTO 'INBOX' is the same thing as KEEP */
122         if ( (!strcasecmp(dest_folder, "INBOX")) || (!strcasecmp(dest_folder, MAILROOM)) ) {
123                 cs->keep = 1;
124                 cs->cancel_implicit_keep = 1;
125                 return SIEVE2_OK;
126         }
127
128         /* Remember what room we came from */
129         safestrncpy(original_room_name, CC->room.QRname, sizeof original_room_name);
130
131         /* First try a mailbox name match (check personal mail folders first) */
132         snprintf(foldername, sizeof foldername, "%010ld.%s", cs->usernum, dest_folder);
133         c = CtdlGetRoom(&CC->room, foldername);
134
135         /* Then a regular room name match (public and private rooms) */
136         if (c != 0) {
137                 safestrncpy(foldername, dest_folder, sizeof foldername);
138                 c = CtdlGetRoom(&CC->room, foldername);
139         }
140
141         if (c != 0) {
142                 syslog(LOG_WARNING, "FILEINTO failed: target <%s> does not exist", dest_folder);
143                 return SIEVE2_ERROR_BADARGS;
144         }
145
146         /* Yes, we actually have to go there */
147         CtdlUserGoto(NULL, 0, 0, NULL, NULL, NULL, NULL);
148
149         c = CtdlSaveMsgPointersInRoom(NULL, &cs->msgnum, 1, 0, NULL, 0);
150
151         /* Go back to the room we came from */
152         if (strcasecmp(original_room_name, CC->room.QRname)) {
153                 CtdlUserGoto(original_room_name, 0, 0, NULL, NULL, NULL, NULL);
154         }
155
156         if (c == 0) {
157                 cs->cancel_implicit_keep = 1;
158                 return SIEVE2_OK;
159         }
160         else {
161                 return SIEVE2_ERROR_BADARGS;
162         }
163 }
164
165
166 /*
167  * Callback function to indicate that a message should be discarded.
168  */
169 int ctdl_discard(sieve2_context_t *s, void *my)
170 {
171         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
172
173         syslog(LOG_DEBUG, "Action is DISCARD");
174
175         /* Cancel the implicit keep.  That's all there is to it. */
176         cs->cancel_implicit_keep = 1;
177         return SIEVE2_OK;
178 }
179
180
181 /*
182  * Callback function to indicate that a message should be rejected
183  */
184 int ctdl_reject(sieve2_context_t *s, void *my)
185 {
186         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
187         char *reject_text = NULL;
188
189         syslog(LOG_DEBUG, "Action is REJECT");
190
191         /* If we don't know who sent the message, do a DISCARD instead. */
192         if (IsEmptyStr(cs->sender)) {
193                 syslog(LOG_INFO, "Unknown sender.  Doing DISCARD instead of REJECT.");
194                 return ctdl_discard(s, my);
195         }
196
197         /* Assemble the reject message. */
198         reject_text = malloc(strlen(sieve2_getvalue_string(s, "message")) + 1024);
199         if (reject_text == NULL) {
200                 return SIEVE2_ERROR_FAIL;
201         }
202
203         sprintf(reject_text, 
204                 "Content-type: text/plain\n"
205                 "\n"
206                 "The message was refused by the recipient's mail filtering program.\n"
207                 "The reason given was as follows:\n"
208                 "\n"
209                 "%s\n"
210                 "\n"
211         ,
212                 sieve2_getvalue_string(s, "message")
213         );
214
215         quickie_message(        /* This delivers the message */
216                 NULL,
217                 cs->envelope_to,
218                 cs->sender,
219                 NULL,
220                 reject_text,
221                 FMT_RFC822,
222                 "Delivery status notification"
223         );
224
225         free(reject_text);
226         cs->cancel_implicit_keep = 1;
227         return SIEVE2_OK;
228 }
229
230
231 /*
232  * Callback function to indicate that a vacation message should be generated
233  */
234 int ctdl_vacation(sieve2_context_t *s, void *my)
235 {
236         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
237         struct sdm_vacation *vptr;
238         int days = 1;
239         const char *message;
240         char *vacamsg_text = NULL;
241         char vacamsg_subject[1024];
242
243         syslog(LOG_DEBUG, "Action is VACATION");
244
245         message = sieve2_getvalue_string(s, "message");
246         if (message == NULL) return SIEVE2_ERROR_BADARGS;
247
248         if (sieve2_getvalue_string(s, "subject") != NULL) {
249                 safestrncpy(vacamsg_subject, sieve2_getvalue_string(s, "subject"), sizeof vacamsg_subject);
250         }
251         else {
252                 snprintf(vacamsg_subject, sizeof vacamsg_subject, "Re: %s", cs->subject);
253         }
254
255         days = sieve2_getvalue_int(s, "days");
256         if (days < 1) days = 1;
257         if (days > MAX_VACATION) days = MAX_VACATION;
258
259         /* Check to see whether we've already alerted this sender that we're on vacation. */
260         for (vptr = cs->u->first_vacation; vptr != NULL; vptr = vptr->next) {
261                 if (!strcasecmp(vptr->fromaddr, cs->sender)) {
262                         if ( (time(NULL) - vptr->timestamp) < (days * 86400) ) {
263                                 syslog(LOG_DEBUG, "Already alerted <%s> recently.", cs->sender);
264                                 return SIEVE2_OK;
265                         }
266                 }
267         }
268
269         /* Assemble the reject message. */
270         vacamsg_text = malloc(strlen(message) + 1024);
271         if (vacamsg_text == NULL) {
272                 return SIEVE2_ERROR_FAIL;
273         }
274
275         sprintf(vacamsg_text, 
276                 "Content-type: text/plain charset=utf-8\n"
277                 "\n"
278                 "%s\n"
279                 "\n"
280         ,
281                 message
282         );
283
284         quickie_message(        /* This delivers the message */
285                 NULL,
286                 cs->envelope_to,
287                 cs->sender,
288                 NULL,
289                 vacamsg_text,
290                 FMT_RFC822,
291                 vacamsg_subject
292         );
293
294         free(vacamsg_text);
295
296         /* Now update the list to reflect the fact that we've alerted this sender.
297          * If they're already in the list, just update the timestamp.
298          */
299         for (vptr = cs->u->first_vacation; vptr != NULL; vptr = vptr->next) {
300                 if (!strcasecmp(vptr->fromaddr, cs->sender)) {
301                         vptr->timestamp = time(NULL);
302                         return SIEVE2_OK;
303                 }
304         }
305
306         /* If we get to this point, create a new record.
307          */
308         vptr = malloc(sizeof(struct sdm_vacation));
309         memset(vptr, 0, sizeof(struct sdm_vacation));
310         vptr->timestamp = time(NULL);
311         safestrncpy(vptr->fromaddr, cs->sender, sizeof vptr->fromaddr);
312         vptr->next = cs->u->first_vacation;
313         cs->u->first_vacation = vptr;
314
315         return SIEVE2_OK;
316 }
317
318
319 /*
320  * Callback function to parse message envelope
321  */
322 int ctdl_getenvelope(sieve2_context_t *s, void *my)
323 {
324         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
325
326         syslog(LOG_DEBUG, "Action is GETENVELOPE");
327         syslog(LOG_DEBUG, "EnvFrom: %s", cs->envelope_from);
328         syslog(LOG_DEBUG, "EnvTo: %s", cs->envelope_to);
329
330         if (cs->envelope_from != NULL) {
331                 if ((cs->envelope_from[0] != '@')&&(cs->envelope_from[strlen(cs->envelope_from)-1] != '@')) {
332                         sieve2_setvalue_string(s, "from", cs->envelope_from);
333                 }
334                 else {
335                         sieve2_setvalue_string(s, "from", "invalid_envelope_from@example.org");
336                 }
337         }
338         else {
339                 sieve2_setvalue_string(s, "from", "null_envelope_from@example.org");
340         }
341
342
343         if (cs->envelope_to != NULL) {
344                 if ((cs->envelope_to[0] != '@') && (cs->envelope_to[strlen(cs->envelope_to)-1] != '@')) {
345                         sieve2_setvalue_string(s, "to", cs->envelope_to);
346                 }
347                 else {
348                         sieve2_setvalue_string(s, "to", "invalid_envelope_to@example.org");
349                 }
350         }
351         else {
352                 sieve2_setvalue_string(s, "to", "null_envelope_to@example.org");
353         }
354
355         return SIEVE2_OK;
356 }
357
358
359
360 /*
361  * Callback function to fetch message size
362  */
363 int ctdl_getsize(sieve2_context_t *s, void *my)
364 {
365         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
366         struct MetaData smi;
367
368         GetMetaData(&smi, cs->msgnum);
369         
370         if (smi.meta_rfc822_length > 0L) {
371                 sieve2_setvalue_int(s, "size", (int)smi.meta_rfc822_length);
372                 return SIEVE2_OK;
373         }
374
375         return SIEVE2_ERROR_UNSUPPORTED;
376 }
377
378
379 /*
380  * Return a pointer to the active Sieve script.
381  * (Caller does NOT own the memory and should not free the returned pointer.)
382  */
383 char *get_active_script(struct sdm_userdata *u) {
384         struct sdm_script *sptr;
385
386         for (sptr=u->first_script; sptr!=NULL; sptr=sptr->next) {
387                 if (sptr->script_active > 0) {
388                         syslog(LOG_DEBUG, "get_active_script() is using script '%s'", sptr->script_name);
389                         return(sptr->script_content);
390                 }
391         }
392
393         syslog(LOG_DEBUG, "get_active_script() found no active script");
394         return(NULL);
395 }
396
397
398 /*
399  * Callback function to retrieve the sieve script
400  */
401 int ctdl_getscript(sieve2_context_t *s, void *my) {
402         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
403
404         char *active_script = get_active_script(cs->u);
405         if (active_script != NULL) {
406                 sieve2_setvalue_string(s, "script", active_script);
407                 return SIEVE2_OK;
408         }
409
410         return SIEVE2_ERROR_GETSCRIPT;
411 }
412
413
414 /*
415  * Callback function to retrieve message headers
416  */
417 int ctdl_getheaders(sieve2_context_t *s, void *my) {
418
419         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
420
421         syslog(LOG_DEBUG, "ctdl_getheaders() was called");
422         sieve2_setvalue_string(s, "allheaders", cs->rfc822headers);
423         return SIEVE2_OK;
424 }
425
426
427 /*
428  * Perform sieve processing for one message (called by sieve_do_room() for each message)
429  */
430 void sieve_do_msg(long msgnum, void *userdata) {
431         struct sdm_userdata *u = (struct sdm_userdata *) userdata;
432         sieve2_context_t *sieve2_context;
433         struct ctdl_sieve my;
434         int res;
435         struct CtdlMessage *msg;
436         int i;
437         size_t headers_len = 0;
438         int len = 0;
439
440         if (u == NULL) {
441                 syslog(LOG_ERR, "Can't process message <%ld> without userdata!", msgnum);
442                 return;
443         }
444
445         sieve2_context = u->sieve2_context;
446
447         syslog(LOG_DEBUG, "Performing sieve processing on msg <%ld>", msgnum);
448
449         /*
450          * Make sure you include message body so you can get those second-level headers ;)
451          */
452         msg = CtdlFetchMessage(msgnum, 1, 1);
453         if (msg == NULL) return;
454
455         /*
456          * Grab the message headers so we can feed them to libSieve.
457          * Use HEADERS_ONLY rather than HEADERS_FAST in order to include second-level headers.
458          */
459         CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
460         CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_ONLY, 0, 1, 0);
461         headers_len = StrLength(CC->redirect_buffer);
462         my.rfc822headers = SmashStrBuf(&CC->redirect_buffer);
463
464         /*
465          * libSieve clobbers the stack if it encounters badly formed
466          * headers.  Sanitize our headers by stripping nonprintable
467          * characters.
468          */
469         for (i=0; i<headers_len; ++i) {
470                 if (!isascii(my.rfc822headers[i])) {
471                         my.rfc822headers[i] = '_';
472                 }
473         }
474
475         my.keep = 0;                            /* Set to 1 to declare an *explicit* keep */
476         my.cancel_implicit_keep = 0;            /* Some actions will cancel the implicit keep */
477         my.usernum = atol(CC->room.QRname);     /* Keep track of the owner of the room's namespace */
478         my.msgnum = msgnum;                     /* Keep track of the message number in our local store */
479         my.u = u;                               /* Hand off a pointer to the rest of this info */
480
481         /* Keep track of the recipient so we can do handling based on it later */
482         process_rfc822_addr(msg->cm_fields[eRecipient], my.recp_user, my.recp_node, my.recp_name);
483
484         /* Keep track of the sender so we can use it for REJECT and VACATION responses */
485         if (!CM_IsEmpty(msg, erFc822Addr)) {
486                 safestrncpy(my.sender, msg->cm_fields[erFc822Addr], sizeof my.sender);
487         }
488         else if (!CM_IsEmpty(msg, eAuthor)) {
489                 safestrncpy(my.sender, msg->cm_fields[eAuthor], sizeof my.sender);
490         }
491         else {
492                 strcpy(my.sender, "");
493         }
494
495         /* Keep track of the subject so we can use it for VACATION responses */
496         if (!CM_IsEmpty(msg, eMsgSubject)) {
497                 safestrncpy(my.subject, msg->cm_fields[eMsgSubject], sizeof my.subject);
498         }
499         else {
500                 strcpy(my.subject, "");
501         }
502
503         /* Keep track of the envelope-from address (use body-from if not found) */
504         if (!CM_IsEmpty(msg, eMessagePath)) {
505                 safestrncpy(my.envelope_from, msg->cm_fields[eMessagePath], sizeof my.envelope_from);
506                 stripallbut(my.envelope_from, '<', '>');
507         }
508         else if (!CM_IsEmpty(msg, erFc822Addr)) {
509                 safestrncpy(my.envelope_from, msg->cm_fields[erFc822Addr], sizeof my.envelope_from);
510                 stripallbut(my.envelope_from, '<', '>');
511         }
512         else {
513                 strcpy(my.envelope_from, "");
514         }
515
516         len = strlen(my.envelope_from);
517         for (i=0; i<len; ++i) {
518                 if (isspace(my.envelope_from[i])) my.envelope_from[i] = '_';
519         }
520         if (haschar(my.envelope_from, '@') == 0) {
521                 strcat(my.envelope_from, "@");
522                 strcat(my.envelope_from, CtdlGetConfigStr("c_fqdn"));
523         }
524
525         /* Keep track of the envelope-to address (use body-to if not found) */
526         if (!CM_IsEmpty(msg, eenVelopeTo)) {
527                 safestrncpy(my.envelope_to, msg->cm_fields[eenVelopeTo], sizeof my.envelope_to);
528                 stripallbut(my.envelope_to, '<', '>');
529         }
530         else if (!CM_IsEmpty(msg, eRecipient)) {
531                 safestrncpy(my.envelope_to, msg->cm_fields[eRecipient], sizeof my.envelope_to);
532                 stripallbut(my.envelope_to, '<', '>');
533         }
534         else {
535                 strcpy(my.envelope_to, "");
536         }
537
538         len = strlen(my.envelope_to);
539         for (i=0; i<len; ++i) {
540                 if (isspace(my.envelope_to[i])) my.envelope_to[i] = '_';
541         }
542         if (haschar(my.envelope_to, '@') == 0) {
543                 strcat(my.envelope_to, "@");
544                 strcat(my.envelope_to, CtdlGetConfigStr("c_fqdn"));
545         }
546
547         CM_Free(msg);
548         
549         syslog(LOG_DEBUG, "Calling sieve2_execute()");
550         res = sieve2_execute(sieve2_context, &my);
551         if (res != SIEVE2_OK) {
552                 syslog(LOG_ERR, "sieve2_execute() returned %d: %s", res, sieve2_errstr(res));
553         }
554
555         free(my.rfc822headers);
556         my.rfc822headers = NULL;
557
558         /*
559          * Delete the message from the inbox unless either we were told not to, or
560          * if no other action was successfully taken.
561          */
562         if ( (!my.keep) && (my.cancel_implicit_keep) ) {
563                 syslog(LOG_DEBUG, "keep is 0 -- deleting message from inbox");
564                 CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
565         }
566
567         syslog(LOG_DEBUG, "Completed sieve processing on msg <%ld>", msgnum);
568         u->lastproc = msgnum;
569
570         return;
571 }
572
573
574
575 /*
576  * Given the on-disk representation of our Sieve config, load
577  * it into an in-memory data structure.
578  */
579 void parse_sieve_config(char *conf, struct sdm_userdata *u) {
580         char *ptr;
581         char *c, *vacrec;
582         char keyword[256];
583         struct sdm_script *sptr;
584         struct sdm_vacation *vptr;
585
586         ptr = conf;
587         while (c = ptr, ptr = bmstrcasestr(ptr, CTDLSIEVECONFIGSEPARATOR), ptr != NULL) {
588                 *ptr = 0;
589                 ptr += strlen(CTDLSIEVECONFIGSEPARATOR);
590
591                 extract_token(keyword, c, 0, '|', sizeof keyword);
592
593                 if (!strcasecmp(keyword, "lastproc")) {
594                         u->lastproc = extract_long(c, 1);
595                 }
596
597                 else if (!strcasecmp(keyword, "script")) {
598                         sptr = malloc(sizeof(struct sdm_script));
599                         extract_token(sptr->script_name, c, 1, '|', sizeof sptr->script_name);
600                         sptr->script_active = extract_int(c, 2);
601                         remove_token(c, 0, '|');
602                         remove_token(c, 0, '|');
603                         remove_token(c, 0, '|');
604                         sptr->script_content = strdup(c);
605                         sptr->next = u->first_script;
606                         u->first_script = sptr;
607                 }
608
609                 else if (!strcasecmp(keyword, "vacation")) {
610
611                         if (c != NULL) while (vacrec=c, c=strchr(c, '\n'), (c != NULL)) {
612
613                                 *c = 0;
614                                 ++c;
615
616                                 if (strncasecmp(vacrec, "vacation|", 9)) {
617                                         vptr = malloc(sizeof(struct sdm_vacation));
618                                         extract_token(vptr->fromaddr, vacrec, 0, '|', sizeof vptr->fromaddr);
619                                         vptr->timestamp = extract_long(vacrec, 1);
620                                         vptr->next = u->first_vacation;
621                                         u->first_vacation = vptr;
622                                 }
623                         }
624                 }
625
626                 /* ignore unknown keywords */
627         }
628 }
629
630
631
632
633
634 /* 
635  * Write our citadel sieve config back to disk
636  * 
637  * (Set yes_write_to_disk to nonzero to make it actually write the config;
638  * otherwise it just frees the data structures.)
639  */
640 void rewrite_ctdl_sieve_config(struct sdm_userdata *u, int yes_write_to_disk) {
641         StrBuf *text;
642         struct sdm_script *sptr;
643         struct sdm_vacation *vptr;
644         
645         text = NewStrBufPlain(NULL, SIZ);
646         StrBufPrintf(text,
647                      "Content-type: application/x-citadel-sieve-config\n"
648                      "\n"
649                      CTDLSIEVECONFIGSEPARATOR
650                      "lastproc|%ld"
651                      CTDLSIEVECONFIGSEPARATOR
652                      ,
653                      u->lastproc
654                 );
655
656         while (u->first_script != NULL) {
657                 StrBufAppendPrintf(text,
658                                    "script|%s|%d|%s" CTDLSIEVECONFIGSEPARATOR,
659                                    u->first_script->script_name,
660                                    u->first_script->script_active,
661                                    u->first_script->script_content
662                         );
663                 sptr = u->first_script;
664                 u->first_script = u->first_script->next;
665                 free(sptr->script_content);
666                 free(sptr);
667         }
668
669         if (u->first_vacation != NULL) {
670
671                 StrBufAppendPrintf(text, "vacation|\n");
672                 while (u->first_vacation != NULL) {
673                         if ( (time(NULL) - u->first_vacation->timestamp) < (MAX_VACATION * 86400)) {
674                                 StrBufAppendPrintf(text, "%s|%ld\n",
675                                                    u->first_vacation->fromaddr,
676                                                    u->first_vacation->timestamp
677                                         );
678                         }
679                         vptr = u->first_vacation;
680                         u->first_vacation = u->first_vacation->next;
681                         free(vptr);
682                 }
683                 StrBufAppendPrintf(text, CTDLSIEVECONFIGSEPARATOR);
684         }
685
686         if (yes_write_to_disk)
687         {
688                 /* Save the config */
689                 quickie_message("Citadel", NULL, NULL, u->config_roomname,
690                                 ChrPtr(text),
691                                 4,
692                                 "Sieve configuration"
693                 );
694                 
695                 /* And delete the old one */
696                 if (u->config_msgnum > 0) {
697                         CtdlDeleteMessages(u->config_roomname, &u->config_msgnum, 1, "");
698                 }
699         }
700
701         FreeStrBuf (&text);
702
703 }
704
705
706 /*
707  * This is our callback registration table for libSieve.
708  */
709 sieve2_callback_t ctdl_sieve_callbacks[] = {
710         { SIEVE2_ACTION_REJECT,         ctdl_reject             },
711         { SIEVE2_ACTION_VACATION,       ctdl_vacation           },
712         { SIEVE2_ERRCALL_PARSE,         ctdl_errparse           },
713         { SIEVE2_ERRCALL_RUNTIME,       ctdl_errexec            },
714         { SIEVE2_ACTION_FILEINTO,       ctdl_fileinto           },
715         { SIEVE2_ACTION_REDIRECT,       ctdl_redirect           },
716         { SIEVE2_ACTION_DISCARD,        ctdl_discard            },
717         { SIEVE2_ACTION_KEEP,           ctdl_keep               },
718         { SIEVE2_SCRIPT_GETSCRIPT,      ctdl_getscript          },
719         { SIEVE2_DEBUG_TRACE,           ctdl_debug              },
720         { SIEVE2_MESSAGE_GETALLHEADERS, ctdl_getheaders         },
721         { SIEVE2_MESSAGE_GETSIZE,       ctdl_getsize            },
722         { SIEVE2_MESSAGE_GETENVELOPE,   ctdl_getenvelope        },
723         { 0 }
724 };
725
726
727 /*
728  * Perform sieve processing for a single room
729  */
730 void sieve_do_room(char *roomname) {
731         
732         struct sdm_userdata u;
733         sieve2_context_t *sieve2_context = NULL;        /* Context for sieve parser */
734         int res;                                        /* Return code from libsieve calls */
735         long orig_lastproc = 0;
736
737         memset(&u, 0, sizeof u);
738
739         /* See if the user who owns this 'mailbox' has any Sieve scripts that
740          * require execution.
741          */
742         snprintf(u.config_roomname, sizeof u.config_roomname, "%010ld.%s", atol(roomname), USERCONFIGROOM);
743         if (CtdlGetRoom(&CC->room, u.config_roomname) != 0) {
744                 syslog(LOG_DEBUG, "<%s> does not exist.  No processing is required.", u.config_roomname);
745                 return;
746         }
747
748         /*
749          * Find the sieve scripts and control record and do something
750          */
751         u.config_msgnum = (-1);
752         CtdlForEachMessage(MSGS_LAST, 1, NULL, SIEVECONFIG, NULL, get_sieve_config_backend, (void *)&u );
753
754         if (u.config_msgnum < 0) {
755                 syslog(LOG_DEBUG, "No Sieve rules exist.  No processing is required.");
756                 return;
757         }
758
759         /*
760          * Check to see whether the script is empty and should not be processed.
761          * A script is considered non-empty if it contains at least one semicolon.
762          */
763         if (
764                 (get_active_script(&u) == NULL)
765                 || (strchr(get_active_script(&u), ';') == NULL)
766         ) {
767                 syslog(LOG_DEBUG, "Sieve script is empty.  No processing is required.");
768                 return;
769         }
770
771         syslog(LOG_DEBUG, "Rules found.  Performing Sieve processing for <%s>", roomname);
772
773         if (CtdlGetRoom(&CC->room, roomname) != 0) {
774                 syslog(LOG_ERR, "ERROR: cannot load <%s>", roomname);
775                 return;
776         }
777
778         /* Initialize the Sieve parser */
779         
780         res = sieve2_alloc(&sieve2_context);
781         if (res != SIEVE2_OK) {
782                 syslog(LOG_ERR, "sieve2_alloc() returned %d: %s", res, sieve2_errstr(res));
783                 return;
784         }
785
786         res = sieve2_callbacks(sieve2_context, ctdl_sieve_callbacks);
787         if (res != SIEVE2_OK) {
788                 syslog(LOG_ERR, "sieve2_callbacks() returned %d: %s", res, sieve2_errstr(res));
789                 goto BAIL;
790         }
791
792         /* Validate the script */
793
794         struct ctdl_sieve my;           /* dummy ctdl_sieve struct just to pass "u" along */
795         memset(&my, 0, sizeof my);
796         my.u = &u;
797         res = sieve2_validate(sieve2_context, &my);
798         if (res != SIEVE2_OK) {
799                 syslog(LOG_ERR, "sieve2_validate() returned %d: %s", res, sieve2_errstr(res));
800                 goto BAIL;
801         }
802
803         /* Do something useful */
804         u.sieve2_context = sieve2_context;
805         orig_lastproc = u.lastproc;
806         CtdlForEachMessage(MSGS_GT, u.lastproc, NULL, NULL, NULL, sieve_do_msg, (void *) &u);
807
808 BAIL:
809         res = sieve2_free(&sieve2_context);
810         if (res != SIEVE2_OK) {
811                 syslog(LOG_ERR, "sieve2_free() returned %d: %s", res, sieve2_errstr(res));
812         }
813
814         /* Rewrite the config if we have to (we're not the user right now) */
815         rewrite_ctdl_sieve_config(&u, (u.lastproc > orig_lastproc) ) ;
816 }
817
818 #endif
819
820
821
822 enum {
823         field_from,             
824         field_tocc,             
825         field_subject,  
826         field_replyto,  
827         field_sender,   
828         field_resentfrom,       
829         field_resentto, 
830         field_envfrom,  
831         field_envto,    
832         field_xmailer,  
833         field_xspamflag,        
834         field_xspamstatus,      
835         field_listid,   
836         field_size,             
837         field_all
838 };
839 char *field_keys[] = {
840         "from",
841         "tocc",
842         "subject",
843         "replyto",
844         "sender",
845         "resentfrom",
846         "resentto",
847         "envfrom",
848         "envto",
849         "xmailer",
850         "xspamflag",
851         "xspamstatus",
852         "listid",
853         "size",
854         "all"
855 };
856
857
858 enum {
859         fcomp_contains,
860         fcomp_notcontains,
861         fcomp_is,
862         fcomp_isnot,
863         fcomp_matches,
864         fcomp_notmatches
865 };
866 char *fcomp_keys[] = {
867         "contains",
868         "notcontains",
869         "is",
870         "isnot",
871         "matches",
872         "notmatches"
873 };
874
875 enum {
876         action_keep,
877         action_discard,
878         action_reject,
879         action_fileinto,
880         action_redirect,
881         action_vacation
882 };
883 char *action_keys[] = {
884         "keep",
885         "discard",
886         "reject",
887         "fileinto",
888         "redirect",
889         "vacation"
890 };
891
892 enum {
893         scomp_larger,
894         scomp_smaller
895 };
896 char *scomp_keys[] = {
897         "larger",
898         "smaller"
899 };
900
901 enum {
902         final_continue,
903         final_stop
904 };
905 char *final_keys[] = {
906         "continue",
907         "stop"
908 };
909
910 struct irule {
911         int field_compare_op;
912         int compared_field;
913         char compared_value[128];
914         int size_compare_op;
915         long compared_size;
916         int action;
917         char file_into[ROOMNAMELEN];
918         char redirect_to[1024];
919         char autoreply_message[SIZ];
920         int final_action;
921 };
922
923 struct inboxrules {
924         long lastproc;
925         int num_rules;
926         struct irule *rules;
927 };
928
929
930 void free_inbox_rules(struct inboxrules *ibr) {
931         free(ibr->rules);
932         free(ibr);
933 }
934
935
936 /*
937  * Convert the serialized inbox rules message to a data type.
938  */
939 struct inboxrules *deserialize_inbox_rules(char *serialized_rules) {
940         int i;
941
942         if (!serialized_rules) {
943                 return NULL;
944         }
945
946         /* Make a copy of the supplied buffer because we're going to shit all over it with strtok_r() */
947         char *sr = strdup(serialized_rules);
948         if (!sr) {
949                 return NULL;
950         }
951
952         struct inboxrules *ibr = malloc(sizeof(struct inboxrules));
953         if (ibr == NULL) {
954                 return NULL;
955         }
956         memset(ibr, 0, sizeof(struct inboxrules));
957
958         char *token; 
959         char *rest = sr;
960         while ((token = strtok_r(rest, "\n", &rest))) {
961
962                 // For backwards compatibility, "# WEBCIT_RULE" is an alias for "rule".
963                 // Prior to version 930, WebCit converted its rules to Sieve scripts, but saved the rules as comments for later re-editing.
964                 // Now, the rules hidden in the comments are the real rules.
965                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
966                         strcpy(token, "rule|"); 
967                         strcpy(&token[5], &token[14]);
968                 }
969
970                 // Lines containing actual rules are double-serialized with Base64.  It seemed like a good idea at the time :(
971                 if (!strncasecmp(token, "rule|", 5)) {
972                         syslog(LOG_DEBUG, "rule: %s", &token[5]);
973                         remove_token(&token[5], 0, '|');
974                         char *decoded_rule = malloc(strlen(token));
975                         CtdlDecodeBase64(decoded_rule, &token[5], strlen(&token[5]));
976                         TRACE;
977                         syslog(LOG_DEBUG, "%s", decoded_rule);  
978
979                         ibr->num_rules++;
980                         ibr->rules = realloc(ibr->rules, (sizeof(struct irule) * ibr->num_rules));
981                         struct irule *new_rule = &ibr->rules[ibr->num_rules - 1];
982                         memset(new_rule, 0, sizeof(struct irule));
983
984                         // We have a rule , now parse it
985                         syslog(LOG_DEBUG, "Detokenizing: %s", decoded_rule);
986                         char rtoken[SIZ];
987                         int nt = num_tokens(decoded_rule, '|');
988                         for (int t=0; t<nt; ++t) {
989                                 extract_token(rtoken, decoded_rule, t, '|', sizeof(rtoken));
990                                 striplt(rtoken);
991                                 syslog(LOG_DEBUG, "Token %d : %s", t, rtoken);
992                                 switch(t) {
993                                         case 1:                                                                 // field to compare
994                                                 for (i=0; i<=field_all; ++i) {
995                                                         if (!strcasecmp(rtoken, field_keys[i])) {
996                                                                 new_rule->compared_field = i;
997                                                         }
998                                                 }
999                                                 break;
1000                                         case 2:                                                                 // field comparison operation
1001                                                 for (i=0; i<=fcomp_notmatches; ++i) {
1002                                                         if (!strcasecmp(rtoken, fcomp_keys[i])) {
1003                                                                 new_rule->field_compare_op = i;
1004                                                         }
1005                                                 }
1006                                                 break;
1007                                         case 3:                                                                 // field comparison value
1008                                                 safestrncpy(new_rule->compared_value, rtoken, sizeof(new_rule->compared_value));
1009                                                 break;
1010                                         case 4:                                                                 // size comparison operation
1011                                                 for (i=0; i<=scomp_smaller; ++i) {
1012                                                         if (!strcasecmp(rtoken, scomp_keys[i])) {
1013                                                                 new_rule->size_compare_op = i;
1014                                                         }
1015                                                 }
1016                                                 break;
1017                                         case 5:                                                                 // size comparison value
1018                                                 new_rule->compared_size = atol(rtoken);
1019                                                 break;
1020                                         case 6:                                                                 // action
1021                                                 for (i=0; i<=action_vacation; ++i) {
1022                                                         if (!strcasecmp(rtoken, action_keys[i])) {
1023                                                                 new_rule->action = i;
1024                                                         }
1025                                                 }
1026                                                 break;
1027                                         case 7:                                                                 // file into (target room)
1028                                                 safestrncpy(new_rule->file_into, rtoken, sizeof(new_rule->file_into));
1029                                                 break;
1030                                         case 8:                                                                 // redirect to (target address)
1031                                                 safestrncpy(new_rule->redirect_to, rtoken, sizeof(new_rule->redirect_to));
1032                                                 break;
1033                                         case 9:                                                                 // autoreply message
1034                                                 safestrncpy(new_rule->autoreply_message, rtoken, sizeof(new_rule->autoreply_message));
1035                                                 break;
1036                                         case 10:                                                                // final_action;
1037                                                 for (i=0; i<=final_stop; ++i) {
1038                                                         if (!strcasecmp(rtoken, final_keys[i])) {
1039                                                                 new_rule->final_action = i;
1040                                                         }
1041                                                 }
1042                                                 break;
1043                                         default:
1044                                                 break;
1045                                 }
1046                         }
1047                         free(decoded_rule);
1048
1049                         // if we re-serialized this now, what would it look like?
1050                         syslog(LOG_DEBUG, "test reserialize: 0|%s|%s|%s|%s|%ld|%s|%s|%s|%s|%s",
1051                                 field_keys[new_rule->compared_field],
1052                                 fcomp_keys[new_rule->field_compare_op],
1053                                 new_rule->compared_value,
1054                                 scomp_keys[new_rule->size_compare_op],
1055                                 new_rule->compared_size,
1056                                 action_keys[new_rule->action],
1057                                 new_rule->file_into,
1058                                 new_rule->redirect_to,
1059                                 new_rule->autoreply_message,
1060                                 final_keys[new_rule->final_action]
1061                         );
1062                         // delete the above after moving it to a reserialize function
1063
1064                 }
1065
1066                 // "lastproc" indicates the newest message number in the inbox that was previously processed by our inbox rules.
1067                 else if (!strncasecmp(token, "lastproc|", 5)) {
1068                         ibr->lastproc = atol(&token[9]);
1069                 }
1070
1071         }
1072
1073         free(sr);               // free our copy of the source buffer that has now been trashed with null bytes...
1074         abort();
1075         return(ibr);            // and return our complex data type to the caller.
1076 }
1077
1078
1079 /*
1080  * A user account is identified as requring inbox processing.
1081  * Do it.
1082  */
1083 void do_inbox_processing_for_user(long usernum) {
1084         if (CtdlGetUserByNumber(&CC->user, usernum) == 0) {
1085                 TRACE;
1086                 if (CC->user.msgnum_inboxrules <= 0) {
1087                         return;                                         // this user has no inbox rules
1088                 }
1089
1090                 struct CtdlMessage *msg;
1091                 char *conf;
1092                 long conflen;
1093         
1094                 msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1, 1);
1095                 if (msg == NULL) {
1096                         return;                                         // config msgnum is set but that message does not exist
1097                 }
1098         
1099                 CM_GetAsField(msg, eMesageText, &conf, &conflen);
1100                 CM_Free(msg);
1101         
1102                 if (conf == NULL) {
1103                         return;                                         // config message exists but body is null
1104                 }
1105
1106                 syslog(LOG_DEBUG, "RULEZ for %s", CC->user.fullname);
1107                 syslog(LOG_DEBUG, "%s", conf);
1108
1109                 // do something now FIXME actually write this
1110
1111                 free(conf);
1112         }
1113 }
1114
1115
1116 /*
1117  * Here is an array of users (by number) who have received messages in their inbox and may require processing.
1118 */
1119 long *users_requiring_inbox_processing = NULL;
1120 int num_urip = 0;
1121 int num_urip_alloc = 0;
1122
1123
1124 /*
1125  * Perform inbox processing for all rooms which require it
1126  */
1127 void perform_inbox_processing(void) {
1128         if (num_urip == 0) {
1129                 return;                                                                                 // no action required
1130         }
1131
1132         for (int i=0; i<num_urip; ++i) {
1133                 do_inbox_processing_for_user(users_requiring_inbox_processing[i]);
1134         }
1135
1136         free(users_requiring_inbox_processing);
1137         users_requiring_inbox_processing = NULL;
1138         num_urip = 0;
1139         num_urip_alloc = 0;
1140 }
1141
1142
1143 /*
1144  * This function is called after a message is saved to a room.
1145  * If it's someone's inbox, we have to check for inbox rules
1146  */
1147 int serv_inboxrules_roomhook(struct ctdlroom *room) {
1148
1149         // Is this someone's inbox?
1150         if (!strcasecmp(&room->QRname[11], MAILROOM)) {
1151                 long usernum = atol(room->QRname);
1152                 if (usernum > 0) {
1153
1154                         // first check to see if this user is already on the list
1155                         if (num_urip > 0) {
1156                                 for (int i=0; i<=num_urip; ++i) {
1157                                         if (users_requiring_inbox_processing[i] == usernum) {           // already on the list!
1158                                                 return(0);
1159                                         }
1160                                 }
1161                         }
1162
1163                         // make room if we need to
1164                         if (num_urip_alloc == 0) {
1165                                 num_urip_alloc = 100;
1166                                 users_requiring_inbox_processing = malloc(sizeof(long) * num_urip_alloc);
1167                         }
1168                         else if (num_urip >= num_urip_alloc) {
1169                                 num_urip_alloc += 100;
1170                                 users_requiring_inbox_processing = realloc(users_requiring_inbox_processing, (sizeof(long) * num_urip_alloc));
1171                         }
1172                         
1173                         // now add the user to the list
1174                         users_requiring_inbox_processing[num_urip++] = usernum;
1175                 }
1176         }
1177
1178         // No errors are possible from this function.
1179         return(0);
1180 }
1181
1182
1183
1184 /*
1185  * Get InBox Rules
1186  *
1187  * This is a client-facing function which fetches the user's inbox rules -- it omits all lines containing anything other than a rule.
1188  */
1189 void cmd_gibr(char *argbuf) {
1190
1191         if (CtdlAccessCheck(ac_logged_in)) return;
1192
1193         cprintf("%d inbox rules for %s\n", LISTING_FOLLOWS, CC->user.fullname);
1194
1195         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1, 1);
1196         if (msg != NULL) {
1197                 if (!CM_IsEmpty(msg, eMesageText)) {
1198
1199
1200                         struct inboxrules *ii = deserialize_inbox_rules(msg->cm_fields[eMesageText]);
1201                         free_inbox_rules(ii);
1202
1203                         char *token; 
1204                         char *rest = msg->cm_fields[eMesageText];
1205                         while ((token = strtok_r(rest, "\n", &rest))) {
1206
1207                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1208                                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
1209                                         strcpy(token, "rule|"); 
1210                                         strcpy(&token[5], &token[14]);
1211                                 }
1212
1213                                 // Output only lines containing rules.
1214                                 if (!strncasecmp(token, "rule|", 5)) {
1215                                         cprintf("%s\n", token); 
1216                                 }
1217                         }
1218                 }
1219                 CM_Free(msg);
1220         }
1221         cprintf("000\n");
1222 }
1223
1224
1225 /*
1226  * Put InBox Rules
1227  *
1228  * User transmits the new inbox rules for the account.  They are inserted into the account, replacing the ones already there.
1229  */
1230 void cmd_pibr(char *argbuf) {
1231         if (CtdlAccessCheck(ac_logged_in)) return;
1232
1233         unbuffer_output();
1234         cprintf("%d send new rules\n", SEND_LISTING);
1235         char *newrules = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1236         StrBuf *NewConfig = NewStrBufPlain("Content-type: application/x-citadel-sieve-config; charset=UTF-8\nContent-transfer-encoding: 8bit\n\n", -1);
1237
1238         char *token; 
1239         char *rest = newrules;
1240         while ((token = strtok_r(rest, "\n", &rest))) {
1241                 // Accept only lines containing rules
1242                 if (!strncasecmp(token, "rule|", 5)) {
1243                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1244                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1245                 }
1246         }
1247         free(newrules);
1248
1249         // Fetch the existing config so we can merge in anything that is NOT a rule 
1250         // (Does not start with "rule|" but has at least one vertical bar)
1251         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1, 1);
1252         if (msg != NULL) {
1253                 if (!CM_IsEmpty(msg, eMesageText)) {
1254                         rest = msg->cm_fields[eMesageText];
1255                         while ((token = strtok_r(rest, "\n", &rest))) {
1256                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1257                                 if ((strncasecmp(token, "# WEBCIT_RULE|", 14)) && (strncasecmp(token, "rule|", 5)) && (haschar(token, '|'))) {
1258                                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1259                                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1260                                 }
1261                         }
1262                 }
1263                 CM_Free(msg);
1264         }
1265
1266         /* we have composed the new configuration , now save it */
1267         long old_msgnum = CC->user.msgnum_inboxrules;
1268         char userconfigroomname[ROOMNAMELEN];
1269         CtdlMailboxName(userconfigroomname, sizeof userconfigroomname, &CC->user, USERCONFIGROOM);
1270         long new_msgnum = quickie_message("Citadel", NULL, NULL, userconfigroomname, ChrPtr(NewConfig), FMT_RFC822, "inbox rules configuration");
1271         FreeStrBuf(&NewConfig);
1272         CtdlGetUserLock(&CC->user, CC->curr_user);
1273         CC->user.msgnum_inboxrules = new_msgnum;
1274         CtdlPutUserLock(&CC->user);
1275         if (old_msgnum > 0) {
1276                 syslog(LOG_DEBUG, "Deleting old message %ld from %s", old_msgnum, userconfigroomname);
1277                 CtdlDeleteMessages(userconfigroomname, &old_msgnum, 1, "");
1278         }
1279 }
1280
1281
1282 CTDL_MODULE_INIT(sieve)
1283 {
1284         if (!threading)
1285         {
1286                 CtdlRegisterProtoHook(cmd_gibr, "GIBR", "Get InBox Rules");
1287                 CtdlRegisterProtoHook(cmd_pibr, "PIBR", "Put InBox Rules");
1288                 CtdlRegisterRoomHook(serv_inboxrules_roomhook);
1289                 CtdlRegisterSessionHook(perform_inbox_processing, EVT_HOUSE, PRIO_HOUSE + 10);
1290         }
1291         
1292         /* return our module name for the log */
1293         return "inboxrules";
1294 }