0baf9320071f7a2de1df5e45eeebf74c113612e9
[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 /*
429  * Perform sieve processing for one message (called by sieve_do_room() for each message)
430  */
431 void sieve_do_msg(long msgnum, void *userdata) {
432         struct sdm_userdata *u = (struct sdm_userdata *) userdata;
433         sieve2_context_t *sieve2_context;
434         struct ctdl_sieve my;
435         int res;
436         struct CtdlMessage *msg;
437         int i;
438         size_t headers_len = 0;
439         int len = 0;
440
441         if (u == NULL) {
442                 syslog(LOG_ERR, "Can't process message <%ld> without userdata!", msgnum);
443                 return;
444         }
445
446         sieve2_context = u->sieve2_context;
447
448         syslog(LOG_DEBUG, "Performing sieve processing on msg <%ld>", msgnum);
449
450         /*
451          * Make sure you include message body so you can get those second-level headers ;)
452          */
453         msg = CtdlFetchMessage(msgnum, 1, 1);
454         if (msg == NULL) return;
455
456         /*
457          * Grab the message headers so we can feed them to libSieve.
458          * Use HEADERS_ONLY rather than HEADERS_FAST in order to include second-level headers.
459          */
460         CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
461         CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_ONLY, 0, 1, 0);
462         headers_len = StrLength(CC->redirect_buffer);
463         my.rfc822headers = SmashStrBuf(&CC->redirect_buffer);
464
465         /*
466          * libSieve clobbers the stack if it encounters badly formed
467          * headers.  Sanitize our headers by stripping nonprintable
468          * characters.
469          */
470         for (i=0; i<headers_len; ++i) {
471                 if (!isascii(my.rfc822headers[i])) {
472                         my.rfc822headers[i] = '_';
473                 }
474         }
475
476         my.keep = 0;                            /* Set to 1 to declare an *explicit* keep */
477         my.cancel_implicit_keep = 0;            /* Some actions will cancel the implicit keep */
478         my.usernum = atol(CC->room.QRname);     /* Keep track of the owner of the room's namespace */
479         my.msgnum = msgnum;                     /* Keep track of the message number in our local store */
480         my.u = u;                               /* Hand off a pointer to the rest of this info */
481
482         /* Keep track of the recipient so we can do handling based on it later */
483         process_rfc822_addr(msg->cm_fields[eRecipient], my.recp_user, my.recp_node, my.recp_name);
484
485         /* Keep track of the sender so we can use it for REJECT and VACATION responses */
486         if (!CM_IsEmpty(msg, erFc822Addr)) {
487                 safestrncpy(my.sender, msg->cm_fields[erFc822Addr], sizeof my.sender);
488         }
489         else if (!CM_IsEmpty(msg, eAuthor)) {
490                 safestrncpy(my.sender, msg->cm_fields[eAuthor], sizeof my.sender);
491         }
492         else {
493                 strcpy(my.sender, "");
494         }
495
496         /* Keep track of the subject so we can use it for VACATION responses */
497         if (!CM_IsEmpty(msg, eMsgSubject)) {
498                 safestrncpy(my.subject, msg->cm_fields[eMsgSubject], sizeof my.subject);
499         }
500         else {
501                 strcpy(my.subject, "");
502         }
503
504         /* Keep track of the envelope-from address (use body-from if not found) */
505         if (!CM_IsEmpty(msg, eMessagePath)) {
506                 safestrncpy(my.envelope_from, msg->cm_fields[eMessagePath], sizeof my.envelope_from);
507                 stripallbut(my.envelope_from, '<', '>');
508         }
509         else if (!CM_IsEmpty(msg, erFc822Addr)) {
510                 safestrncpy(my.envelope_from, msg->cm_fields[erFc822Addr], sizeof my.envelope_from);
511                 stripallbut(my.envelope_from, '<', '>');
512         }
513         else {
514                 strcpy(my.envelope_from, "");
515         }
516
517         len = strlen(my.envelope_from);
518         for (i=0; i<len; ++i) {
519                 if (isspace(my.envelope_from[i])) my.envelope_from[i] = '_';
520         }
521         if (haschar(my.envelope_from, '@') == 0) {
522                 strcat(my.envelope_from, "@");
523                 strcat(my.envelope_from, CtdlGetConfigStr("c_fqdn"));
524         }
525
526         /* Keep track of the envelope-to address (use body-to if not found) */
527         if (!CM_IsEmpty(msg, eenVelopeTo)) {
528                 safestrncpy(my.envelope_to, msg->cm_fields[eenVelopeTo], sizeof my.envelope_to);
529                 stripallbut(my.envelope_to, '<', '>');
530         }
531         else if (!CM_IsEmpty(msg, eRecipient)) {
532                 safestrncpy(my.envelope_to, msg->cm_fields[eRecipient], sizeof my.envelope_to);
533                 stripallbut(my.envelope_to, '<', '>');
534         }
535         else {
536                 strcpy(my.envelope_to, "");
537         }
538
539         len = strlen(my.envelope_to);
540         for (i=0; i<len; ++i) {
541                 if (isspace(my.envelope_to[i])) my.envelope_to[i] = '_';
542         }
543         if (haschar(my.envelope_to, '@') == 0) {
544                 strcat(my.envelope_to, "@");
545                 strcat(my.envelope_to, CtdlGetConfigStr("c_fqdn"));
546         }
547
548         CM_Free(msg);
549         
550         syslog(LOG_DEBUG, "Calling sieve2_execute()");
551         res = sieve2_execute(sieve2_context, &my);
552         if (res != SIEVE2_OK) {
553                 syslog(LOG_ERR, "sieve2_execute() returned %d: %s", res, sieve2_errstr(res));
554         }
555
556         free(my.rfc822headers);
557         my.rfc822headers = NULL;
558
559         /*
560          * Delete the message from the inbox unless either we were told not to, or
561          * if no other action was successfully taken.
562          */
563         if ( (!my.keep) && (my.cancel_implicit_keep) ) {
564                 syslog(LOG_DEBUG, "keep is 0 -- deleting message from inbox");
565                 CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
566         }
567
568         syslog(LOG_DEBUG, "Completed sieve processing on msg <%ld>", msgnum);
569         u->lastproc = msgnum;
570
571         return;
572 }
573
574
575
576 /*
577  * Given the on-disk representation of our Sieve config, load
578  * it into an in-memory data structure.
579  */
580 void parse_sieve_config(char *conf, struct sdm_userdata *u) {
581         char *ptr;
582         char *c, *vacrec;
583         char keyword[256];
584         struct sdm_script *sptr;
585         struct sdm_vacation *vptr;
586
587         ptr = conf;
588         while (c = ptr, ptr = bmstrcasestr(ptr, CTDLSIEVECONFIGSEPARATOR), ptr != NULL) {
589                 *ptr = 0;
590                 ptr += strlen(CTDLSIEVECONFIGSEPARATOR);
591
592                 extract_token(keyword, c, 0, '|', sizeof keyword);
593
594                 if (!strcasecmp(keyword, "lastproc")) {
595                         u->lastproc = extract_long(c, 1);
596                 }
597
598                 else if (!strcasecmp(keyword, "script")) {
599                         sptr = malloc(sizeof(struct sdm_script));
600                         extract_token(sptr->script_name, c, 1, '|', sizeof sptr->script_name);
601                         sptr->script_active = extract_int(c, 2);
602                         remove_token(c, 0, '|');
603                         remove_token(c, 0, '|');
604                         remove_token(c, 0, '|');
605                         sptr->script_content = strdup(c);
606                         sptr->next = u->first_script;
607                         u->first_script = sptr;
608                 }
609
610                 else if (!strcasecmp(keyword, "vacation")) {
611
612                         if (c != NULL) while (vacrec=c, c=strchr(c, '\n'), (c != NULL)) {
613
614                                 *c = 0;
615                                 ++c;
616
617                                 if (strncasecmp(vacrec, "vacation|", 9)) {
618                                         vptr = malloc(sizeof(struct sdm_vacation));
619                                         extract_token(vptr->fromaddr, vacrec, 0, '|', sizeof vptr->fromaddr);
620                                         vptr->timestamp = extract_long(vacrec, 1);
621                                         vptr->next = u->first_vacation;
622                                         u->first_vacation = vptr;
623                                 }
624                         }
625                 }
626
627                 /* ignore unknown keywords */
628         }
629 }
630
631
632
633
634
635 /* 
636  * Write our citadel sieve config back to disk
637  * 
638  * (Set yes_write_to_disk to nonzero to make it actually write the config;
639  * otherwise it just frees the data structures.)
640  */
641 void rewrite_ctdl_sieve_config(struct sdm_userdata *u, int yes_write_to_disk) {
642         StrBuf *text;
643         struct sdm_script *sptr;
644         struct sdm_vacation *vptr;
645         
646         text = NewStrBufPlain(NULL, SIZ);
647         StrBufPrintf(text,
648                      "Content-type: application/x-citadel-sieve-config\n"
649                      "\n"
650                      CTDLSIEVECONFIGSEPARATOR
651                      "lastproc|%ld"
652                      CTDLSIEVECONFIGSEPARATOR
653                      ,
654                      u->lastproc
655                 );
656
657         while (u->first_script != NULL) {
658                 StrBufAppendPrintf(text,
659                                    "script|%s|%d|%s" CTDLSIEVECONFIGSEPARATOR,
660                                    u->first_script->script_name,
661                                    u->first_script->script_active,
662                                    u->first_script->script_content
663                         );
664                 sptr = u->first_script;
665                 u->first_script = u->first_script->next;
666                 free(sptr->script_content);
667                 free(sptr);
668         }
669
670         if (u->first_vacation != NULL) {
671
672                 StrBufAppendPrintf(text, "vacation|\n");
673                 while (u->first_vacation != NULL) {
674                         if ( (time(NULL) - u->first_vacation->timestamp) < (MAX_VACATION * 86400)) {
675                                 StrBufAppendPrintf(text, "%s|%ld\n",
676                                                    u->first_vacation->fromaddr,
677                                                    u->first_vacation->timestamp
678                                         );
679                         }
680                         vptr = u->first_vacation;
681                         u->first_vacation = u->first_vacation->next;
682                         free(vptr);
683                 }
684                 StrBufAppendPrintf(text, CTDLSIEVECONFIGSEPARATOR);
685         }
686
687         if (yes_write_to_disk)
688         {
689                 /* Save the config */
690                 quickie_message("Citadel", NULL, NULL, u->config_roomname,
691                                 ChrPtr(text),
692                                 4,
693                                 "Sieve configuration"
694                 );
695                 
696                 /* And delete the old one */
697                 if (u->config_msgnum > 0) {
698                         CtdlDeleteMessages(u->config_roomname, &u->config_msgnum, 1, "");
699                 }
700         }
701
702         FreeStrBuf (&text);
703
704 }
705
706
707 /*
708  * This is our callback registration table for libSieve.
709  */
710 sieve2_callback_t ctdl_sieve_callbacks[] = {
711         { SIEVE2_ACTION_REJECT,         ctdl_reject             },
712         { SIEVE2_ACTION_VACATION,       ctdl_vacation           },
713         { SIEVE2_ERRCALL_PARSE,         ctdl_errparse           },
714         { SIEVE2_ERRCALL_RUNTIME,       ctdl_errexec            },
715         { SIEVE2_ACTION_FILEINTO,       ctdl_fileinto           },
716         { SIEVE2_ACTION_REDIRECT,       ctdl_redirect           },
717         { SIEVE2_ACTION_DISCARD,        ctdl_discard            },
718         { SIEVE2_ACTION_KEEP,           ctdl_keep               },
719         { SIEVE2_SCRIPT_GETSCRIPT,      ctdl_getscript          },
720         { SIEVE2_DEBUG_TRACE,           ctdl_debug              },
721         { SIEVE2_MESSAGE_GETALLHEADERS, ctdl_getheaders         },
722         { SIEVE2_MESSAGE_GETSIZE,       ctdl_getsize            },
723         { SIEVE2_MESSAGE_GETENVELOPE,   ctdl_getenvelope        },
724         { 0 }
725 };
726
727
728 /*
729  * Perform sieve processing for a single room
730  */
731 void sieve_do_room(char *roomname) {
732         
733         struct sdm_userdata u;
734         sieve2_context_t *sieve2_context = NULL;        /* Context for sieve parser */
735         int res;                                        /* Return code from libsieve calls */
736         long orig_lastproc = 0;
737
738         memset(&u, 0, sizeof u);
739
740         /* See if the user who owns this 'mailbox' has any Sieve scripts that
741          * require execution.
742          */
743         snprintf(u.config_roomname, sizeof u.config_roomname, "%010ld.%s", atol(roomname), USERCONFIGROOM);
744         if (CtdlGetRoom(&CC->room, u.config_roomname) != 0) {
745                 syslog(LOG_DEBUG, "<%s> does not exist.  No processing is required.", u.config_roomname);
746                 return;
747         }
748
749         /*
750          * Find the sieve scripts and control record and do something
751          */
752         u.config_msgnum = (-1);
753         CtdlForEachMessage(MSGS_LAST, 1, NULL, SIEVECONFIG, NULL, get_sieve_config_backend, (void *)&u );
754
755         if (u.config_msgnum < 0) {
756                 syslog(LOG_DEBUG, "No Sieve rules exist.  No processing is required.");
757                 return;
758         }
759
760         /*
761          * Check to see whether the script is empty and should not be processed.
762          * A script is considered non-empty if it contains at least one semicolon.
763          */
764         if (
765                 (get_active_script(&u) == NULL)
766                 || (strchr(get_active_script(&u), ';') == NULL)
767         ) {
768                 syslog(LOG_DEBUG, "Sieve script is empty.  No processing is required.");
769                 return;
770         }
771
772         syslog(LOG_DEBUG, "Rules found.  Performing Sieve processing for <%s>", roomname);
773
774         if (CtdlGetRoom(&CC->room, roomname) != 0) {
775                 syslog(LOG_ERR, "ERROR: cannot load <%s>", roomname);
776                 return;
777         }
778
779         /* Initialize the Sieve parser */
780         
781         res = sieve2_alloc(&sieve2_context);
782         if (res != SIEVE2_OK) {
783                 syslog(LOG_ERR, "sieve2_alloc() returned %d: %s", res, sieve2_errstr(res));
784                 return;
785         }
786
787         res = sieve2_callbacks(sieve2_context, ctdl_sieve_callbacks);
788         if (res != SIEVE2_OK) {
789                 syslog(LOG_ERR, "sieve2_callbacks() returned %d: %s", res, sieve2_errstr(res));
790                 goto BAIL;
791         }
792
793         /* Validate the script */
794
795         struct ctdl_sieve my;           /* dummy ctdl_sieve struct just to pass "u" along */
796         memset(&my, 0, sizeof my);
797         my.u = &u;
798         res = sieve2_validate(sieve2_context, &my);
799         if (res != SIEVE2_OK) {
800                 syslog(LOG_ERR, "sieve2_validate() returned %d: %s", res, sieve2_errstr(res));
801                 goto BAIL;
802         }
803
804         /* Do something useful */
805         u.sieve2_context = sieve2_context;
806         orig_lastproc = u.lastproc;
807         CtdlForEachMessage(MSGS_GT, u.lastproc, NULL, NULL, NULL, sieve_do_msg, (void *) &u);
808
809 BAIL:
810         res = sieve2_free(&sieve2_context);
811         if (res != SIEVE2_OK) {
812                 syslog(LOG_ERR, "sieve2_free() returned %d: %s", res, sieve2_errstr(res));
813         }
814
815         /* Rewrite the config if we have to (we're not the user right now) */
816         rewrite_ctdl_sieve_config(&u, (u.lastproc > orig_lastproc) ) ;
817 }
818
819 #endif
820
821
822
823 /*
824  * A user account is identified as requring inbox processing.
825  * Do it.
826  */
827 void do_inbox_processing_for_user(long usernum) {
828         if (CtdlGetUserByNumber(&CC->user, usernum) == 0) {
829                 TRACE;
830                 if (CC->user.msgnum_inboxrules <= 0) {
831                         return;                                         // this user has no inbox rules
832                 }
833
834                 struct CtdlMessage *msg;
835                 char *conf;
836                 long conflen;
837         
838                 msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1, 1);
839                 if (msg == NULL) {
840                         return;                                         // config msgnum is set but that message does not exist
841                 }
842         
843                 CM_GetAsField(msg, eMesageText, &conf, &conflen);
844                 CM_Free(msg);
845         
846                 if (conf == NULL) {
847                         return;                                         // config message exists but body is null
848                 }
849
850
851                 syslog(LOG_DEBUG, "RULEZ for %s", CC->user.fullname);
852                 syslog(LOG_DEBUG, "%s", conf);
853
854                 // do something now FIXME actually write this
855
856                 free(conf);
857         }
858 }
859
860
861 /*
862  * Here is an array of users (by number) who have received messages in their inbox and may require processing.
863 */
864 long *users_requiring_inbox_processing = NULL;
865 int num_urip = 0;
866 int num_urip_alloc = 0;
867
868
869 /*
870  * Perform inbox processing for all rooms which require it
871  */
872 void perform_inbox_processing(void) {
873         if (num_urip == 0) {
874                 return;                                                                                 // no action required
875         }
876
877         for (int i=0; i<num_urip; ++i) {
878                 do_inbox_processing_for_user(users_requiring_inbox_processing[i]);
879         }
880
881         free(users_requiring_inbox_processing);
882         users_requiring_inbox_processing = NULL;
883         num_urip = 0;
884         num_urip_alloc = 0;
885 }
886
887
888 /*
889  * This function is called after a message is saved to a room.
890  * If it's someone's inbox, we have to check for inbox rules
891  */
892 int serv_inboxrules_roomhook(struct ctdlroom *room) {
893
894         // Is this someone's inbox?
895         if (!strcasecmp(&room->QRname[11], MAILROOM)) {
896                 long usernum = atol(room->QRname);
897                 if (usernum > 0) {
898
899                         // first check to see if this user is already on the list
900                         if (num_urip > 0) {
901                                 for (int i=0; i<=num_urip; ++i) {
902                                         if (users_requiring_inbox_processing[i] == usernum) {           // already on the list!
903                                                 return(0);
904                                         }
905                                 }
906                         }
907
908                         // make room if we need to
909                         if (num_urip_alloc == 0) {
910                                 num_urip_alloc = 100;
911                                 users_requiring_inbox_processing = malloc(sizeof(long) * num_urip_alloc);
912                         }
913                         else if (num_urip >= num_urip_alloc) {
914                                 num_urip_alloc += 100;
915                                 users_requiring_inbox_processing = realloc(users_requiring_inbox_processing, (sizeof(long) * num_urip_alloc));
916                         }
917                         
918                         // now add the user to the list
919                         users_requiring_inbox_processing[num_urip++] = usernum;
920                 }
921         }
922
923         // No errors are possible from this function.
924         return(0);
925 }
926
927
928 enum {
929         field_from,             
930         field_tocc,             
931         field_subject,  
932         field_replyto,  
933         field_sender,   
934         field_resentfrom,       
935         field_resentto, 
936         field_envfrom,  
937         field_envto,    
938         field_xmailer,  
939         field_xspamflag,        
940         field_xspamstatus,      
941         field_listid,   
942         field_size,             
943         field_all
944 };
945 char *field_keys[] = {
946         "from",
947         "tocc",
948         "subject",
949         "replyto",
950         "sender",
951         "resentfrom",
952         "resentto",
953         "envfrom",
954         "envto",
955         "xmailer",
956         "xspamflag",
957         "xspamstatus",
958         "listid",
959         "size",
960         "all"
961 };
962
963
964 enum {
965         fcomp_contains,
966         fcomp_notcontains,
967         fcomp_is,
968         fcomp_isnot,
969         fcomp_matches,
970         fcomp_notmatches
971 };
972 char *fcomp_keys[] = {
973         "contains",
974         "notcontains",
975         "is",
976         "isnot",
977         "matches",
978         "notmatches"
979 };
980
981 enum {
982         action_keep,
983         action_discard,
984         action_reject,
985         action_fileinto,
986         action_redirect,
987         action_vacation
988 };
989 char *action_keys[] = {
990         "keep",
991         "discard",
992         "reject",
993         "fileinto",
994         "redirect",
995         "vacation"
996 };
997
998 enum {
999         scomp_larger,
1000         scomp_smaller
1001 };
1002 char *scomp_keys[] = {
1003         "larger",
1004         "smaller"
1005 };
1006
1007 enum {
1008         final_continue,
1009         final_stop
1010 };
1011 char *final_keys[] = {
1012         "continue",
1013         "stop"
1014 };
1015
1016 struct irule {
1017         int field_compare_op;
1018         int compared_field;
1019         char compared_value[128];
1020         int size_compare_op;
1021         long compared_size;
1022         int action;
1023         char file_into[ROOMNAMELEN];
1024         char redirect_to[1024];
1025         char autoreply_message[SIZ];
1026         int final_action;
1027 };
1028
1029 struct inboxrules {
1030         long lastproc;
1031         int num_rules;
1032         struct irule *rules;
1033 };
1034
1035
1036 void free_inbox_rules(struct inboxrules *ibr) {
1037         free(ibr->rules);
1038         free(ibr);
1039 }
1040
1041
1042 /*
1043  * Convert the serialized inbox rules message to a data type.
1044  */
1045 struct inboxrules *deserialize_inbox_rules(char *serialized_rules) {
1046         int i;
1047
1048         if (!serialized_rules) {
1049                 return NULL;
1050         }
1051
1052         /* Make a copy of the supplied buffer because we're going to shit all over it with strtok_r() */
1053         char *sr = strdup(serialized_rules);
1054         if (!sr) {
1055                 return NULL;
1056         }
1057
1058         struct inboxrules *ibr = malloc(sizeof(struct inboxrules));
1059         if (ibr == NULL) {
1060                 return NULL;
1061         }
1062         memset(ibr, 0, sizeof(struct inboxrules));
1063
1064         char *token; 
1065         char *rest = sr;
1066         while ((token = strtok_r(rest, "\n", &rest))) {
1067
1068                 // For backwards compatibility, "# WEBCIT_RULE" is an alias for "rule".
1069                 // Prior to version 930, WebCit converted its rules to Sieve scripts, but saved the rules as comments for later re-editing.
1070                 // Now, the rules hidden in the comments are the real rules.
1071                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
1072                         strcpy(token, "rule|"); 
1073                         strcpy(&token[5], &token[14]);
1074                 }
1075
1076                 // Lines containing actual rules are double-serialized with Base64.  It seemed like a good idea at the time :(
1077                 if (!strncasecmp(token, "rule|", 5)) {
1078                         syslog(LOG_DEBUG, "rule: %s", &token[5]);
1079                         remove_token(&token[5], 0, '|');
1080                         char *decoded_rule = malloc(strlen(token));
1081                         CtdlDecodeBase64(decoded_rule, &token[5], strlen(&token[5]));
1082                         TRACE;
1083                         syslog(LOG_DEBUG, "%s", decoded_rule);  
1084
1085                         ibr->num_rules++;
1086                         ibr->rules = realloc(ibr->rules, (sizeof(struct irule) * ibr->num_rules));
1087                         struct irule *new_rule = &ibr->rules[ibr->num_rules - 1];
1088                         memset(new_rule, 0, sizeof(struct irule));
1089
1090                         // We have a rule , now parse it
1091                         syslog(LOG_DEBUG, "Detokenizing: %s", decoded_rule);
1092                         char rtoken[SIZ];
1093                         int nt = num_tokens(decoded_rule, '|');
1094                         for (int t=0; t<nt; ++t) {
1095                                 extract_token(rtoken, decoded_rule, t, '|', sizeof(rtoken));
1096                                 striplt(rtoken);
1097                                 syslog(LOG_DEBUG, "Token %d : %s", t, rtoken);
1098                                 switch(t) {
1099                                         case 1:                                                                 // field to compare
1100                                                 for (i=0; i<=field_all; ++i) {
1101                                                         if (!strcasecmp(rtoken, field_keys[i])) {
1102                                                                 new_rule->compared_field = i;
1103                                                         }
1104                                                 }
1105                                                 break;
1106                                         case 2:                                                                 // field comparison operation
1107                                                 for (i=0; i<=fcomp_notmatches; ++i) {
1108                                                         if (!strcasecmp(rtoken, fcomp_keys[i])) {
1109                                                                 new_rule->field_compare_op = i;
1110                                                         }
1111                                                 }
1112                                                 break;
1113                                         case 3:                                                                 // field comparison value
1114                                                 safestrncpy(new_rule->compared_value, rtoken, sizeof(new_rule->compared_value));
1115                                                 break;
1116                                         case 4:                                                                 // size comparison operation
1117                                                 for (i=0; i<=scomp_smaller; ++i) {
1118                                                         if (!strcasecmp(rtoken, scomp_keys[i])) {
1119                                                                 new_rule->size_compare_op = i;
1120                                                         }
1121                                                 }
1122                                                 break;
1123                                         case 5:                                                                 // size comparison value
1124                                                 new_rule->compared_size = atol(rtoken);
1125                                                 break;
1126                                         case 6:                                                                 // action
1127                                                 for (i=0; i<=action_vacation; ++i) {
1128                                                         if (!strcasecmp(rtoken, action_keys[i])) {
1129                                                                 new_rule->action = i;
1130                                                         }
1131                                                 }
1132                                                 break;
1133                                         case 7:                                                                 // file into (target room)
1134                                                 safestrncpy(new_rule->file_into, rtoken, sizeof(new_rule->file_into));
1135                                                 break;
1136                                         case 8:                                                                 // redirect to (target address)
1137                                                 safestrncpy(new_rule->redirect_to, rtoken, sizeof(new_rule->redirect_to));
1138                                                 break;
1139                                         case 9:                                                                 // autoreply message
1140                                                 safestrncpy(new_rule->autoreply_message, rtoken, sizeof(new_rule->autoreply_message));
1141                                                 break;
1142                                         case 10:                                                                // final_action;
1143                                                 for (i=0; i<=final_stop; ++i) {
1144                                                         if (!strcasecmp(rtoken, final_keys[i])) {
1145                                                                 new_rule->final_action = i;
1146                                                         }
1147                                                 }
1148                                                 break;
1149                                         default:
1150                                                 break;
1151                                 }
1152                         }
1153                         free(decoded_rule);
1154
1155                         // if we re-serialized this now, what would it look like?
1156                         syslog(LOG_DEBUG, "test reserialize: 0|%s|%s|%s|%s|%ld|%s|%s|%s|%s|%s",
1157                                 field_keys[new_rule->compared_field],
1158                                 fcomp_keys[new_rule->field_compare_op],
1159                                 new_rule->compared_value,
1160                                 scomp_keys[new_rule->size_compare_op],
1161                                 new_rule->compared_size,
1162                                 action_keys[new_rule->action],
1163                                 new_rule->file_into,
1164                                 new_rule->redirect_to,
1165                                 new_rule->autoreply_message,
1166                                 final_keys[new_rule->final_action]
1167                         );
1168                         // delete the above after moving it to a reserialize function
1169
1170                 }
1171
1172                 // "lastproc" indicates the newest message number in the inbox that was previously processed by our inbox rules.
1173                 else if (!strncasecmp(token, "lastproc|", 5)) {
1174                         ibr->lastproc = atol(&token[9]);
1175                 }
1176
1177         }
1178
1179         free(sr);               // free our copy of the source buffer that has now been trashed with null bytes...
1180         abort();
1181         return(ibr);            // and return our complex data type to the caller.
1182 }
1183
1184
1185 /*
1186  * Get InBox Rules
1187  *
1188  * This is a client-facing function which fetches the user's inbox rules -- it omits all lines containing anything other than a rule.
1189  */
1190 void cmd_gibr(char *argbuf) {
1191
1192         if (CtdlAccessCheck(ac_logged_in)) return;
1193
1194         cprintf("%d inbox rules for %s\n", LISTING_FOLLOWS, CC->user.fullname);
1195
1196         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1, 1);
1197         if (msg != NULL) {
1198                 if (!CM_IsEmpty(msg, eMesageText)) {
1199
1200
1201                         struct inboxrules *ii = deserialize_inbox_rules(msg->cm_fields[eMesageText]);
1202                         free_inbox_rules(ii);
1203
1204                         char *token; 
1205                         char *rest = msg->cm_fields[eMesageText];
1206                         while ((token = strtok_r(rest, "\n", &rest))) {
1207
1208                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1209                                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
1210                                         strcpy(token, "rule|"); 
1211                                         strcpy(&token[5], &token[14]);
1212                                 }
1213
1214                                 // Output only lines containing rules.
1215                                 if (!strncasecmp(token, "rule|", 5)) {
1216                                         cprintf("%s\n", token); 
1217                                 }
1218                         }
1219                 }
1220                 CM_Free(msg);
1221         }
1222         cprintf("000\n");
1223 }
1224
1225
1226 /*
1227  * Put InBox Rules
1228  *
1229  * User transmits the new inbox rules for the account.  They are inserted into the account, replacing the ones already there.
1230  */
1231 void cmd_pibr(char *argbuf) {
1232         if (CtdlAccessCheck(ac_logged_in)) return;
1233
1234         unbuffer_output();
1235         cprintf("%d send new rules\n", SEND_LISTING);
1236         char *newrules = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1237         StrBuf *NewConfig = NewStrBufPlain("Content-type: application/x-citadel-sieve-config; charset=UTF-8\nContent-transfer-encoding: 8bit\n\n", -1);
1238
1239         char *token; 
1240         char *rest = newrules;
1241         while ((token = strtok_r(rest, "\n", &rest))) {
1242                 // Accept only lines containing rules
1243                 if (!strncasecmp(token, "rule|", 5)) {
1244                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1245                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1246                 }
1247         }
1248         free(newrules);
1249
1250         // Fetch the existing config so we can merge in anything that is NOT a rule 
1251         // (Does not start with "rule|" but has at least one vertical bar)
1252         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1, 1);
1253         if (msg != NULL) {
1254                 if (!CM_IsEmpty(msg, eMesageText)) {
1255                         rest = msg->cm_fields[eMesageText];
1256                         while ((token = strtok_r(rest, "\n", &rest))) {
1257                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1258                                 if ((strncasecmp(token, "# WEBCIT_RULE|", 14)) && (strncasecmp(token, "rule|", 5)) && (haschar(token, '|'))) {
1259                                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1260                                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1261                                 }
1262                         }
1263                 }
1264                 CM_Free(msg);
1265         }
1266
1267         /* we have composed the new configuration , now save it */
1268         long old_msgnum = CC->user.msgnum_inboxrules;
1269         char userconfigroomname[ROOMNAMELEN];
1270         CtdlMailboxName(userconfigroomname, sizeof userconfigroomname, &CC->user, USERCONFIGROOM);
1271         long new_msgnum = quickie_message("Citadel", NULL, NULL, userconfigroomname, ChrPtr(NewConfig), FMT_RFC822, "inbox rules configuration");
1272         FreeStrBuf(&NewConfig);
1273         CtdlGetUserLock(&CC->user, CC->curr_user);
1274         CC->user.msgnum_inboxrules = new_msgnum;
1275         CtdlPutUserLock(&CC->user);
1276         if (old_msgnum > 0) {
1277                 syslog(LOG_DEBUG, "Deleting old message %ld from %s", old_msgnum, userconfigroomname);
1278                 CtdlDeleteMessages(userconfigroomname, &old_msgnum, 1, "");
1279         }
1280 }
1281
1282
1283 CTDL_MODULE_INIT(sieve)
1284 {
1285         if (!threading)
1286         {
1287                 CtdlRegisterProtoHook(cmd_gibr, "GIBR", "Get InBox Rules");
1288                 CtdlRegisterProtoHook(cmd_pibr, "PIBR", "Put InBox Rules");
1289                 CtdlRegisterRoomHook(serv_inboxrules_roomhook);
1290                 CtdlRegisterSessionHook(perform_inbox_processing, EVT_HOUSE, PRIO_HOUSE + 10);
1291         }
1292         
1293         /* return our module name for the log */
1294         return "inboxrules";
1295 }