Implemented the 'stop' final action
[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);
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
95 /*
96  * Callback function to file a message into a different mailbox
97  */
98 int ctdl_fileinto(sieve2_context_t *s, void *my)
99 {
100         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
101         const char *dest_folder = sieve2_getvalue_string(s, "mailbox");
102         int c;
103         char foldername[256];
104         char original_room_name[ROOMNAMELEN];
105
106         syslog(LOG_DEBUG, "Action is FILEINTO, destination is <%s>", dest_folder);
107
108         /* FILEINTO 'INBOX' is the same thing as KEEP */
109         if ( (!strcasecmp(dest_folder, "INBOX")) || (!strcasecmp(dest_folder, MAILROOM)) ) {
110                 cs->keep = 1;
111                 cs->cancel_implicit_keep = 1;
112                 return SIEVE2_OK;
113         }
114
115         /* Remember what room we came from */
116         safestrncpy(original_room_name, CC->room.QRname, sizeof original_room_name);
117
118         /* First try a mailbox name match (check personal mail folders first) */
119         snprintf(foldername, sizeof foldername, "%010ld.%s", cs->usernum, dest_folder);
120         c = CtdlGetRoom(&CC->room, foldername);
121
122         /* Then a regular room name match (public and private rooms) */
123         if (c != 0) {
124                 safestrncpy(foldername, dest_folder, sizeof foldername);
125                 c = CtdlGetRoom(&CC->room, foldername);
126         }
127
128         if (c != 0) {
129                 syslog(LOG_WARNING, "FILEINTO failed: target <%s> does not exist", dest_folder);
130                 return SIEVE2_ERROR_BADARGS;
131         }
132
133         /* Yes, we actually have to go there */
134         CtdlUserGoto(NULL, 0, 0, NULL, NULL, NULL, NULL);
135
136         c = CtdlSaveMsgPointersInRoom(NULL, &cs->msgnum, 1, 0, NULL, 0);
137
138         /* Go back to the room we came from */
139         if (strcasecmp(original_room_name, CC->room.QRname)) {
140                 CtdlUserGoto(original_room_name, 0, 0, NULL, NULL, NULL, NULL);
141         }
142
143         if (c == 0) {
144                 cs->cancel_implicit_keep = 1;
145                 return SIEVE2_OK;
146         }
147         else {
148                 return SIEVE2_ERROR_BADARGS;
149         }
150 }
151
152
153 /*
154  * Callback function to indicate that a message should be discarded.
155  */
156 int ctdl_discard(sieve2_context_t *s, void *my)
157 {
158         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
159
160         syslog(LOG_DEBUG, "Action is DISCARD");
161
162         /* Cancel the implicit keep.  That's all there is to it. */
163         cs->cancel_implicit_keep = 1;
164         return SIEVE2_OK;
165 }
166
167
168 /*
169  * Callback function to indicate that a message should be rejected
170  */
171 int ctdl_reject(sieve2_context_t *s, void *my)
172 {
173         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
174         char *reject_text = NULL;
175
176         syslog(LOG_DEBUG, "Action is REJECT");
177
178         /* If we don't know who sent the message, do a DISCARD instead. */
179         if (IsEmptyStr(cs->sender)) {
180                 syslog(LOG_INFO, "Unknown sender.  Doing DISCARD instead of REJECT.");
181                 return ctdl_discard(s, my);
182         }
183
184         /* Assemble the reject message. */
185         reject_text = malloc(strlen(sieve2_getvalue_string(s, "message")) + 1024);
186         if (reject_text == NULL) {
187                 return SIEVE2_ERROR_FAIL;
188         }
189
190         sprintf(reject_text, 
191                 "Content-type: text/plain\n"
192                 "\n"
193                 "The message was refused by the recipient's mail filtering program.\n"
194                 "The reason given was as follows:\n"
195                 "\n"
196                 "%s\n"
197                 "\n"
198         ,
199                 sieve2_getvalue_string(s, "message")
200         );
201
202         quickie_message(        /* This delivers the message */
203                 NULL,
204                 cs->envelope_to,
205                 cs->sender,
206                 NULL,
207                 reject_text,
208                 FMT_RFC822,
209                 "Delivery status notification"
210         );
211
212         free(reject_text);
213         cs->cancel_implicit_keep = 1;
214         return SIEVE2_OK;
215 }
216
217
218 /*
219  * Callback function to indicate that a vacation message should be generated
220  */
221 int ctdl_vacation(sieve2_context_t *s, void *my)
222 {
223         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
224         struct sdm_vacation *vptr;
225         int days = 1;
226         const char *message;
227         char *vacamsg_text = NULL;
228         char vacamsg_subject[1024];
229
230         syslog(LOG_DEBUG, "Action is VACATION");
231
232         message = sieve2_getvalue_string(s, "message");
233         if (message == NULL) return SIEVE2_ERROR_BADARGS;
234
235         if (sieve2_getvalue_string(s, "subject") != NULL) {
236                 safestrncpy(vacamsg_subject, sieve2_getvalue_string(s, "subject"), sizeof vacamsg_subject);
237         }
238         else {
239                 snprintf(vacamsg_subject, sizeof vacamsg_subject, "Re: %s", cs->subject);
240         }
241
242         days = sieve2_getvalue_int(s, "days");
243         if (days < 1) days = 1;
244         if (days > MAX_VACATION) days = MAX_VACATION;
245
246         /* Check to see whether we've already alerted this sender that we're on vacation. */
247         for (vptr = cs->u->first_vacation; vptr != NULL; vptr = vptr->next) {
248                 if (!strcasecmp(vptr->fromaddr, cs->sender)) {
249                         if ( (time(NULL) - vptr->timestamp) < (days * 86400) ) {
250                                 syslog(LOG_DEBUG, "Already alerted <%s> recently.", cs->sender);
251                                 return SIEVE2_OK;
252                         }
253                 }
254         }
255
256         /* Assemble the reject message. */
257         vacamsg_text = malloc(strlen(message) + 1024);
258         if (vacamsg_text == NULL) {
259                 return SIEVE2_ERROR_FAIL;
260         }
261
262         sprintf(vacamsg_text, 
263                 "Content-type: text/plain charset=utf-8\n"
264                 "\n"
265                 "%s\n"
266                 "\n"
267         ,
268                 message
269         );
270
271         quickie_message(        /* This delivers the message */
272                 NULL,
273                 cs->envelope_to,
274                 cs->sender,
275                 NULL,
276                 vacamsg_text,
277                 FMT_RFC822,
278                 vacamsg_subject
279         );
280
281         free(vacamsg_text);
282
283         /* Now update the list to reflect the fact that we've alerted this sender.
284          * If they're already in the list, just update the timestamp.
285          */
286         for (vptr = cs->u->first_vacation; vptr != NULL; vptr = vptr->next) {
287                 if (!strcasecmp(vptr->fromaddr, cs->sender)) {
288                         vptr->timestamp = time(NULL);
289                         return SIEVE2_OK;
290                 }
291         }
292
293         /* If we get to this point, create a new record.
294          */
295         vptr = malloc(sizeof(struct sdm_vacation));
296         memset(vptr, 0, sizeof(struct sdm_vacation));
297         vptr->timestamp = time(NULL);
298         safestrncpy(vptr->fromaddr, cs->sender, sizeof vptr->fromaddr);
299         vptr->next = cs->u->first_vacation;
300         cs->u->first_vacation = vptr;
301
302         return SIEVE2_OK;
303 }
304
305
306 /*
307  * Callback function to parse message envelope
308  */
309 int ctdl_getenvelope(sieve2_context_t *s, void *my)
310 {
311         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
312
313         syslog(LOG_DEBUG, "Action is GETENVELOPE");
314         syslog(LOG_DEBUG, "EnvFrom: %s", cs->envelope_from);
315         syslog(LOG_DEBUG, "EnvTo: %s", cs->envelope_to);
316
317         if (cs->envelope_from != NULL) {
318                 if ((cs->envelope_from[0] != '@')&&(cs->envelope_from[strlen(cs->envelope_from)-1] != '@')) {
319                         sieve2_setvalue_string(s, "from", cs->envelope_from);
320                 }
321                 else {
322                         sieve2_setvalue_string(s, "from", "invalid_envelope_from@example.org");
323                 }
324         }
325         else {
326                 sieve2_setvalue_string(s, "from", "null_envelope_from@example.org");
327         }
328
329
330         if (cs->envelope_to != NULL) {
331                 if ((cs->envelope_to[0] != '@') && (cs->envelope_to[strlen(cs->envelope_to)-1] != '@')) {
332                         sieve2_setvalue_string(s, "to", cs->envelope_to);
333                 }
334                 else {
335                         sieve2_setvalue_string(s, "to", "invalid_envelope_to@example.org");
336                 }
337         }
338         else {
339                 sieve2_setvalue_string(s, "to", "null_envelope_to@example.org");
340         }
341
342         return SIEVE2_OK;
343 }
344
345
346
347
348 /*
349  * Given the on-disk representation of our Sieve config, load
350  * it into an in-memory data structure.
351  */
352 void parse_sieve_config(char *conf, struct sdm_userdata *u) {
353         char *ptr;
354         char *c, *vacrec;
355         char keyword[256];
356         struct sdm_script *sptr;
357         struct sdm_vacation *vptr;
358
359         ptr = conf;
360         while (c = ptr, ptr = bmstrcasestr(ptr, CTDLSIEVECONFIGSEPARATOR), ptr != NULL) {
361                 *ptr = 0;
362                 ptr += strlen(CTDLSIEVECONFIGSEPARATOR);
363
364                 extract_token(keyword, c, 0, '|', sizeof keyword);
365
366                 if (!strcasecmp(keyword, "lastproc")) {
367                         u->lastproc = extract_long(c, 1);
368                 }
369
370                 else if (!strcasecmp(keyword, "script")) {
371                         sptr = malloc(sizeof(struct sdm_script));
372                         extract_token(sptr->script_name, c, 1, '|', sizeof sptr->script_name);
373                         sptr->script_active = extract_int(c, 2);
374                         remove_token(c, 0, '|');
375                         remove_token(c, 0, '|');
376                         remove_token(c, 0, '|');
377                         sptr->script_content = strdup(c);
378                         sptr->next = u->first_script;
379                         u->first_script = sptr;
380                 }
381
382                 else if (!strcasecmp(keyword, "vacation")) {
383
384                         if (c != NULL) while (vacrec=c, c=strchr(c, '\n'), (c != NULL)) {
385
386                                 *c = 0;
387                                 ++c;
388
389                                 if (strncasecmp(vacrec, "vacation|", 9)) {
390                                         vptr = malloc(sizeof(struct sdm_vacation));
391                                         extract_token(vptr->fromaddr, vacrec, 0, '|', sizeof vptr->fromaddr);
392                                         vptr->timestamp = extract_long(vacrec, 1);
393                                         vptr->next = u->first_vacation;
394                                         u->first_vacation = vptr;
395                                 }
396                         }
397                 }
398
399                 /* ignore unknown keywords */
400         }
401 }
402
403
404
405
406
407 /* 
408  * Write our citadel sieve config back to disk
409  * 
410  * (Set yes_write_to_disk to nonzero to make it actually write the config;
411  * otherwise it just frees the data structures.)
412  */
413 void rewrite_ctdl_sieve_config(struct sdm_userdata *u, int yes_write_to_disk) {
414         StrBuf *text;
415         struct sdm_script *sptr;
416         struct sdm_vacation *vptr;
417         
418         text = NewStrBufPlain(NULL, SIZ);
419         StrBufPrintf(text,
420                      "Content-type: application/x-citadel-sieve-config\n"
421                      "\n"
422                      CTDLSIEVECONFIGSEPARATOR
423                      "lastproc|%ld"
424                      CTDLSIEVECONFIGSEPARATOR
425                      ,
426                      u->lastproc
427                 );
428
429         while (u->first_script != NULL) {
430                 StrBufAppendPrintf(text,
431                                    "script|%s|%d|%s" CTDLSIEVECONFIGSEPARATOR,
432                                    u->first_script->script_name,
433                                    u->first_script->script_active,
434                                    u->first_script->script_content
435                         );
436                 sptr = u->first_script;
437                 u->first_script = u->first_script->next;
438                 free(sptr->script_content);
439                 free(sptr);
440         }
441
442         if (u->first_vacation != NULL) {
443
444                 StrBufAppendPrintf(text, "vacation|\n");
445                 while (u->first_vacation != NULL) {
446                         if ( (time(NULL) - u->first_vacation->timestamp) < (MAX_VACATION * 86400)) {
447                                 StrBufAppendPrintf(text, "%s|%ld\n",
448                                                    u->first_vacation->fromaddr,
449                                                    u->first_vacation->timestamp
450                                         );
451                         }
452                         vptr = u->first_vacation;
453                         u->first_vacation = u->first_vacation->next;
454                         free(vptr);
455                 }
456                 StrBufAppendPrintf(text, CTDLSIEVECONFIGSEPARATOR);
457         }
458
459         if (yes_write_to_disk)
460         {
461                 /* Save the config */
462                 quickie_message("Citadel", NULL, NULL, u->config_roomname,
463                                 ChrPtr(text),
464                                 4,
465                                 "Sieve configuration"
466                 );
467                 
468                 /* And delete the old one */
469                 if (u->config_msgnum > 0) {
470                         CtdlDeleteMessages(u->config_roomname, &u->config_msgnum, 1, "");
471                 }
472         }
473
474         FreeStrBuf (&text);
475
476 }
477
478
479 #endif
480
481
482 /*
483  * The next sections are enums and keys that drive the serialize/deserialize functions for the inbox rules/state configuration.
484  */
485
486 // Fields to be compared
487 enum {
488         field_from,             
489         field_tocc,             
490         field_subject,  
491         field_replyto,  
492         field_sender,   
493         field_resentfrom,       
494         field_resentto, 
495         field_envfrom,  
496         field_envto,    
497         field_xmailer,  
498         field_xspamflag,        
499         field_xspamstatus,      
500         field_listid,   
501         field_size,             
502         field_all
503 };
504 char *field_keys[] = {
505         "from",
506         "tocc",
507         "subject",
508         "replyto",
509         "sender",
510         "resentfrom",
511         "resentto",
512         "envfrom",
513         "envto",
514         "xmailer",
515         "xspamflag",
516         "xspamstatus",
517         "listid",
518         "size",
519         "all"
520 };
521
522 // Field comparison operators
523 enum {
524         fcomp_contains,
525         fcomp_notcontains,
526         fcomp_is,
527         fcomp_isnot,
528         fcomp_matches,
529         fcomp_notmatches
530 };
531 char *fcomp_keys[] = {
532         "contains",
533         "notcontains",
534         "is",
535         "isnot",
536         "matches",
537         "notmatches"
538 };
539
540 // Actions
541 enum {
542         action_keep,
543         action_discard,
544         action_reject,
545         action_fileinto,
546         action_redirect,
547         action_vacation
548 };
549 char *action_keys[] = {
550         "keep",
551         "discard",
552         "reject",
553         "fileinto",
554         "redirect",
555         "vacation"
556 };
557
558 // Size comparison operators
559 enum {
560         scomp_larger,
561         scomp_smaller
562 };
563 char *scomp_keys[] = {
564         "larger",
565         "smaller"
566 };
567
568 // Final actions
569 enum {
570         final_continue,
571         final_stop
572 };
573 char *final_keys[] = {
574         "continue",
575         "stop"
576 };
577
578 // This data structure represents ONE inbox rule within the configuration.
579 struct irule {
580         int compared_field;
581         int field_compare_op;
582         char compared_value[128];
583         int size_compare_op;
584         long compared_size;
585         int action;
586         char file_into[ROOMNAMELEN];
587         char redirect_to[1024];
588         char autoreply_message[SIZ];
589         int final_action;
590 };
591
592 // This data structure represents the entire inbox rules configuration AND current state for a single user.
593 struct inboxrules {
594         long lastproc;
595         int num_rules;
596         struct irule *rules;
597 };
598
599
600 // Destructor for 'struct inboxrules'
601 void free_inbox_rules(struct inboxrules *ibr) {
602         free(ibr->rules);
603         free(ibr);
604 }
605
606
607 // Constructor for 'struct inboxrules' that deserializes the configuration from text input.
608 struct inboxrules *deserialize_inbox_rules(char *serialized_rules) {
609         int i;
610
611         if (!serialized_rules) {
612                 return NULL;
613         }
614
615         /* Make a copy of the supplied buffer because we're going to shit all over it with strtok_r() */
616         char *sr = strdup(serialized_rules);
617         if (!sr) {
618                 return NULL;
619         }
620
621         struct inboxrules *ibr = malloc(sizeof(struct inboxrules));
622         if (ibr == NULL) {
623                 return NULL;
624         }
625         memset(ibr, 0, sizeof(struct inboxrules));
626
627         char *token; 
628         char *rest = sr;
629         while ((token = strtok_r(rest, "\n", &rest))) {
630
631                 // For backwards compatibility, "# WEBCIT_RULE" is an alias for "rule".
632                 // Prior to version 930, WebCit converted its rules to Sieve scripts, but saved the rules as comments for later re-editing.
633                 // Now, the rules hidden in the comments become the real rules.
634                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
635                         strcpy(token, "rule|"); 
636                         strcpy(&token[5], &token[14]);
637                 }
638
639                 // Lines containing actual rules are double-serialized with Base64.  It seemed like a good idea at the time :(
640                 if (!strncasecmp(token, "rule|", 5)) {
641                         remove_token(&token[5], 0, '|');
642                         char *decoded_rule = malloc(strlen(token));
643                         CtdlDecodeBase64(decoded_rule, &token[5], strlen(&token[5]));
644                         ibr->num_rules++;
645                         ibr->rules = realloc(ibr->rules, (sizeof(struct irule) * ibr->num_rules));
646                         struct irule *new_rule = &ibr->rules[ibr->num_rules - 1];
647                         memset(new_rule, 0, sizeof(struct irule));
648
649                         // We have a rule , now parse it
650                         char rtoken[SIZ];
651                         int nt = num_tokens(decoded_rule, '|');
652                         for (int t=0; t<nt; ++t) {
653                                 extract_token(rtoken, decoded_rule, t, '|', sizeof(rtoken));
654                                 striplt(rtoken);
655                                 switch(t) {
656                                         case 1:                                                                 // field to compare
657                                                 for (i=0; i<=field_all; ++i) {
658                                                         if (!strcasecmp(rtoken, field_keys[i])) {
659                                                                 new_rule->compared_field = i;
660                                                         }
661                                                 }
662                                                 break;
663                                         case 2:                                                                 // field comparison operation
664                                                 for (i=0; i<=fcomp_notmatches; ++i) {
665                                                         if (!strcasecmp(rtoken, fcomp_keys[i])) {
666                                                                 new_rule->field_compare_op = i;
667                                                         }
668                                                 }
669                                                 break;
670                                         case 3:                                                                 // field comparison value
671                                                 safestrncpy(new_rule->compared_value, rtoken, sizeof(new_rule->compared_value));
672                                                 break;
673                                         case 4:                                                                 // size comparison operation
674                                                 for (i=0; i<=scomp_smaller; ++i) {
675                                                         if (!strcasecmp(rtoken, scomp_keys[i])) {
676                                                                 new_rule->size_compare_op = i;
677                                                         }
678                                                 }
679                                                 break;
680                                         case 5:                                                                 // size comparison value
681                                                 new_rule->compared_size = atol(rtoken);
682                                                 break;
683                                         case 6:                                                                 // action
684                                                 for (i=0; i<=action_vacation; ++i) {
685                                                         if (!strcasecmp(rtoken, action_keys[i])) {
686                                                                 new_rule->action = i;
687                                                         }
688                                                 }
689                                                 break;
690                                         case 7:                                                                 // file into (target room)
691                                                 safestrncpy(new_rule->file_into, rtoken, sizeof(new_rule->file_into));
692                                                 break;
693                                         case 8:                                                                 // redirect to (target address)
694                                                 safestrncpy(new_rule->redirect_to, rtoken, sizeof(new_rule->redirect_to));
695                                                 break;
696                                         case 9:                                                                 // autoreply message
697                                                 safestrncpy(new_rule->autoreply_message, rtoken, sizeof(new_rule->autoreply_message));
698                                                 break;
699                                         case 10:                                                                // final_action;
700                                                 for (i=0; i<=final_stop; ++i) {
701                                                         if (!strcasecmp(rtoken, final_keys[i])) {
702                                                                 new_rule->final_action = i;
703                                                         }
704                                                 }
705                                                 break;
706                                         default:
707                                                 break;
708                                 }
709                         }
710                         free(decoded_rule);
711
712                         // if we re-serialized this now, what would it look like?
713                         syslog(LOG_DEBUG, "test reserialize: 0|%s|%s|%s|%s|%ld|%s|%s|%s|%s|%s",
714                                 field_keys[new_rule->compared_field],
715                                 fcomp_keys[new_rule->field_compare_op],
716                                 new_rule->compared_value,
717                                 scomp_keys[new_rule->size_compare_op],
718                                 new_rule->compared_size,
719                                 action_keys[new_rule->action],
720                                 new_rule->file_into,
721                                 new_rule->redirect_to,
722                                 new_rule->autoreply_message,
723                                 final_keys[new_rule->final_action]
724                         );
725                         // delete the above after moving it to a reserialize function
726
727                 }
728
729                 // "lastproc" indicates the newest message number in the inbox that was previously processed by our inbox rules.
730                 else if (!strncasecmp(token, "lastproc|", 5)) {
731                         ibr->lastproc = atol(&token[9]);
732                 }
733
734         }
735
736         free(sr);               // free our copy of the source buffer that has now been trashed with null bytes...
737         return(ibr);            // and return our complex data type to the caller.
738 }
739
740
741 /*
742  * Process a single message.  We know the room, the user, the rules, the message number, etc.
743  */
744 void inbox_do_msg(long msgnum, void *userdata) {
745         struct inboxrules *ii = (struct inboxrules *) userdata;
746         struct CtdlMessage *msg = NULL;         // If we are loading a message to process, put it here.
747         int headers_loaded = 0;                 // Did we load the headers yet?  Do it only once.
748         int body_loaded = 0;                    // Did we load the message body yet?  Do it only once.
749         int metadata_loaded = 0;                // Did we load the metadata yet?  Do it only once.
750         struct MetaData smi;                    // If we are loading the metadata to compare, put it here.
751         int rule_activated = 0;                 // On each rule, this is set if the compare succeeds and the rule activates.
752         char compare_me[SIZ];                   // On each rule, we will store the field to be compared here.
753         int i;
754
755         syslog(LOG_DEBUG, "inboxrules: processing message #%ld which is higher than %ld, we are in %s", msgnum, ii->lastproc, CC->room.QRname);
756
757         if (ii->num_rules <= 0) {
758                 syslog(LOG_DEBUG, "inboxrules: rule set is empty");
759                 return;
760         }
761
762         for (i=0; i<ii->num_rules; ++i) {
763                 syslog(LOG_DEBUG, "inboxrules: processing rule %d is %s", i, field_keys[ ii->rules[i].compared_field ]);
764                 rule_activated = 0;
765
766                 // Before doing a field compare, check to see if we have the correct parts of the message in memory.
767
768                 switch(ii->rules[i].compared_field) {
769                         // These fields require loading only the top-level headers
770                         case field_from:                // From:
771                         case field_tocc:                // To: or Cc:
772                         case field_subject:             // Subject:
773                         case field_replyto:             // Reply-to:
774                         case field_listid:              // List-ID:
775                         case field_envto:               // Envelope-to:
776                         case field_envfrom:             // Return-path:
777                                 if (!headers_loaded) {
778                                         syslog(LOG_DEBUG, "inboxrules: loading headers for message %ld", msgnum);
779                                         msg = CtdlFetchMessage(msgnum, 0);
780                                         if (!msg) {
781                                                 return;
782                                         }
783                                         headers_loaded = 1;
784                                 }
785                                 break;
786                         // These fields are not stored as Citadel headers, and therefore require a full message load.
787                         case field_sender:
788                         case field_resentfrom:
789                         case field_resentto:
790                         case field_xmailer:
791                         case field_xspamflag:
792                         case field_xspamstatus:
793                                 if (!body_loaded) {
794                                         syslog(LOG_DEBUG, "inboxrules: loading all of message %ld", msgnum);
795                                         if (msg != NULL) {
796                                                 CM_Free(msg);
797                                         }
798                                         msg = CtdlFetchMessage(msgnum, 1);
799                                         if (!msg) {
800                                                 return;
801                                         }
802                                         headers_loaded = 1;
803                                         body_loaded = 1;
804                                 }
805                                 break;
806                         case field_size:
807                                 if (!metadata_loaded) {
808                                         syslog(LOG_DEBUG, "inboxrules: loading metadata for message %ld", msgnum);
809                                         GetMetaData(&smi, msgnum);
810                                         metadata_loaded = 1;
811                                 }
812                                 break;
813                         case field_all:
814                                 syslog(LOG_DEBUG, "inboxrules: this is an always-on rule");
815                                 break;
816                         default:
817                                 syslog(LOG_DEBUG, "inboxrules: unknown rule key");
818                 }
819
820                 // If the rule involves a field comparison, load the field to be compared.
821                 compare_me[0] = 0;
822                 switch(ii->rules[i].compared_field) {
823
824                         case field_from:                // From:
825                                 if (!IsEmptyStr(msg->cm_fields[erFc822Addr])) {
826                                         safestrncpy(compare_me, msg->cm_fields[erFc822Addr], sizeof compare_me);
827                                 }
828                                 break;
829                         case field_tocc:                // To: or Cc:
830                                 if (!IsEmptyStr(msg->cm_fields[eRecipient])) {
831                                         safestrncpy(compare_me, msg->cm_fields[eRecipient], sizeof compare_me);
832                                 }
833                                 if (!IsEmptyStr(msg->cm_fields[eCarbonCopY])) {
834                                         if (!IsEmptyStr(compare_me)) {
835                                                 strcat(compare_me, ",");
836                                         }
837                                         safestrncpy(&compare_me[strlen(compare_me)], msg->cm_fields[eCarbonCopY], (sizeof compare_me - strlen(compare_me)));
838                                 }
839                                 break;
840                         case field_subject:             // Subject:
841                                 if (!IsEmptyStr(msg->cm_fields[eMsgSubject])) {
842                                         safestrncpy(compare_me, msg->cm_fields[eMsgSubject], sizeof compare_me);
843                                 }
844                                 break;
845                         case field_replyto:             // Reply-to:
846                                 if (!IsEmptyStr(msg->cm_fields[eReplyTo])) {
847                                         safestrncpy(compare_me, msg->cm_fields[eReplyTo], sizeof compare_me);
848                                 }
849                                 break;
850                         case field_listid:              // List-ID:
851                                 if (!IsEmptyStr(msg->cm_fields[eListID])) {
852                                         safestrncpy(compare_me, msg->cm_fields[eListID], sizeof compare_me);
853                                 }
854                                 break;
855                         case field_envto:               // Envelope-to:
856                                 if (!IsEmptyStr(msg->cm_fields[eenVelopeTo])) {
857                                         safestrncpy(compare_me, msg->cm_fields[eenVelopeTo], sizeof compare_me);
858                                 }
859                                 break;
860                         case field_envfrom:             // Return-path:
861                                 if (!IsEmptyStr(msg->cm_fields[eMessagePath])) {
862                                         safestrncpy(compare_me, msg->cm_fields[eMessagePath], sizeof compare_me);
863                                 }
864                                 break;
865
866                         case field_sender:
867                         case field_resentfrom:
868                         case field_resentto:
869                         case field_xmailer:
870                         case field_xspamflag:
871                         case field_xspamstatus:
872
873                         default:
874                                 break;
875                 }
876
877                 // Message data to compare is loaded, now do something.
878                 switch(ii->rules[i].compared_field) {
879                         case field_from:                // From:
880                         case field_tocc:                // To: or Cc:
881                         case field_subject:             // Subject:
882                         case field_replyto:             // Reply-to:
883                         case field_listid:              // List-ID:
884                         case field_envto:               // Envelope-to:
885                         case field_envfrom:             // Return-path:
886                         case field_sender:
887                         case field_resentfrom:
888                         case field_resentto:
889                         case field_xmailer:
890                         case field_xspamflag:
891                         case field_xspamstatus:
892
893                                 // For all of the above fields, we can compare the field we've loaded into the buffer.
894                                 syslog(LOG_DEBUG, "Value of field to compare is: <%s>", compare_me);
895                                 switch(ii->rules[i].field_compare_op) {
896                                         case fcomp_contains:
897                                         case fcomp_matches:
898                                                 rule_activated = (bmstrcasestr(compare_me, ii->rules[i].compared_value) ? 1 : 0);
899                                                 syslog(LOG_DEBUG, "Does %s contain %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
900                                                 break;
901                                         case fcomp_notcontains:
902                                         case fcomp_notmatches:
903                                                 rule_activated = (bmstrcasestr(compare_me, ii->rules[i].compared_value) ? 0 : 1);
904                                                 syslog(LOG_DEBUG, "Does %s contain %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
905                                                 break;
906                                         case fcomp_is:
907                                                 rule_activated = (strcasecmp(compare_me, ii->rules[i].compared_value) ? 0 : 1);
908                                                 syslog(LOG_DEBUG, "Does %s equal %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
909                                                 break;
910                                         case fcomp_isnot:
911                                                 rule_activated = (strcasecmp(compare_me, ii->rules[i].compared_value) ? 1 : 0);
912                                                 syslog(LOG_DEBUG, "Does %s equal %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
913                                                 break;
914                                 }
915                                 break;
916
917                         case field_size:
918                                 rule_activated = 0;
919                                 syslog(LOG_DEBUG, "comparing actual message size %ld to rule message size %ld", smi.meta_rfc822_length, ii->rules[i].compared_size);
920                                 switch(ii->rules[i].field_compare_op) {
921                                         case scomp_larger:
922                                                 rule_activated = ((smi.meta_rfc822_length > ii->rules[i].compared_size) ? 1 : 0);
923                                                 syslog(LOG_DEBUG, "Is %ld larger than %ld? %s", smi.meta_rfc822_length, ii->rules[i].compared_size, (smi.meta_rfc822_length > ii->rules[i].compared_size) ? "yes":"no");
924                                                 break;
925                                         case scomp_smaller:
926                                                 rule_activated = ((smi.meta_rfc822_length < ii->rules[i].compared_size) ? 1 : 0);
927                                                 syslog(LOG_DEBUG, "Is %ld smaller than %ld? %s", smi.meta_rfc822_length, ii->rules[i].compared_size, (smi.meta_rfc822_length < ii->rules[i].compared_size) ? "yes":"no");
928                                                 break;
929                                 }
930                                 break;
931                         case field_all:                 // The "all messages" rule ALWAYS triggers
932                                 rule_activated = 1;
933                                 break;
934                         default:                        // no matches, fall through and do nothing
935                                 syslog(LOG_DEBUG, "inboxrules: an unknown field comparison was encountered");
936                                 rule_activated = 0;
937                                 break;
938                 }
939
940                 // FIXME you are here YOU ARE HERE next write the code to take action
941                 if (rule_activated) {
942                         syslog(LOG_DEBUG, "\033[32m\033[7mrule activated\033[0m");              // FIXME remove color
943
944
945                         // do action
946
947
948
949                         // do final action (anything other than "stop" means continue)
950                         if (ii->rules[i].final_action == final_stop) {
951                                 syslog(LOG_DEBUG, "\033[33m\033[7mSTOP\033[0m");                // FIXME remove color
952                                 i = ii->num_rules + 1;
953                         }
954
955
956                 }
957                 else {
958                         syslog(LOG_DEBUG, "\033[31m\033[7mrule not activated\033[0m");          // FIXME remove color
959                 }
960         
961         }
962
963         if (msg != NULL) {
964                 CM_Free(msg);
965         }
966 }
967
968
969 /*
970  * A user account is identified as requring inbox processing.
971  * Do it.
972  */
973 void do_inbox_processing_for_user(long usernum) {
974         struct CtdlMessage *msg;
975         struct inboxrules *ii;
976         char roomname[ROOMNAMELEN];
977
978         if (CtdlGetUserByNumber(&CC->user, usernum) == 0) {
979                 if (CC->user.msgnum_inboxrules <= 0) {
980                         return;                                         // this user has no inbox rules
981                 }
982
983                 msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
984                 if (msg == NULL) {
985                         return;                                         // config msgnum is set but that message does not exist
986                 }
987         
988                 ii = deserialize_inbox_rules(msg->cm_fields[eMesageText]);
989                 CM_Free(msg);
990         
991                 if (ii == NULL) {
992                         return;                                         // config message exists but body is null
993                 }
994
995                 syslog(LOG_DEBUG, "inboxrules: for %s", CC->user.fullname);
996
997                 // Go to the user's inbox room and process all new messages
998                 snprintf(roomname, sizeof roomname, "%010ld.%s", usernum, MAILROOM);
999                 if (CtdlGetRoom(&CC->room, roomname) == 0) {
1000                         CtdlForEachMessage(MSGS_GT, ii->lastproc, NULL, NULL, NULL, inbox_do_msg, (void *) ii);
1001                 }
1002
1003                 // FIXME reserialize our inbox rules/state and write changes back to the config room
1004                 free_inbox_rules(ii);
1005         }
1006 }
1007
1008
1009 /*
1010  * Here is an array of users (by number) who have received messages in their inbox and may require processing.
1011  */
1012 long *users_requiring_inbox_processing = NULL;
1013 int num_urip = 0;
1014 int num_urip_alloc = 0;
1015
1016
1017 /*
1018  * Perform inbox processing for all rooms which require it
1019  */
1020 void perform_inbox_processing(void) {
1021         if (num_urip == 0) {
1022                 return;                                                                                 // no action required
1023         }
1024
1025         for (int i=0; i<num_urip; ++i) {
1026                 do_inbox_processing_for_user(users_requiring_inbox_processing[i]);
1027         }
1028
1029         free(users_requiring_inbox_processing);
1030         users_requiring_inbox_processing = NULL;
1031         num_urip = 0;
1032         num_urip_alloc = 0;
1033 }
1034
1035
1036 /*
1037  * This function is called after a message is saved to a room.
1038  * If it's someone's inbox, we have to check for inbox rules
1039  */
1040 int serv_inboxrules_roomhook(struct ctdlroom *room) {
1041
1042         // Is this someone's inbox?
1043         if (!strcasecmp(&room->QRname[11], MAILROOM)) {
1044                 long usernum = atol(room->QRname);
1045                 if (usernum > 0) {
1046
1047                         // first check to see if this user is already on the list
1048                         if (num_urip > 0) {
1049                                 for (int i=0; i<=num_urip; ++i) {
1050                                         if (users_requiring_inbox_processing[i] == usernum) {           // already on the list!
1051                                                 return(0);
1052                                         }
1053                                 }
1054                         }
1055
1056                         // make room if we need to
1057                         if (num_urip_alloc == 0) {
1058                                 num_urip_alloc = 100;
1059                                 users_requiring_inbox_processing = malloc(sizeof(long) * num_urip_alloc);
1060                         }
1061                         else if (num_urip >= num_urip_alloc) {
1062                                 num_urip_alloc += 100;
1063                                 users_requiring_inbox_processing = realloc(users_requiring_inbox_processing, (sizeof(long) * num_urip_alloc));
1064                         }
1065                         
1066                         // now add the user to the list
1067                         users_requiring_inbox_processing[num_urip++] = usernum;
1068                 }
1069         }
1070
1071         // No errors are possible from this function.
1072         return(0);
1073 }
1074
1075
1076
1077 /*
1078  * Get InBox Rules
1079  *
1080  * This is a client-facing function which fetches the user's inbox rules -- it omits all lines containing anything other than a rule.
1081  * 
1082  * hmmmmm ... should we try to rebuild this in terms of deserialize_inbox_rules() instread?
1083  */
1084 void cmd_gibr(char *argbuf) {
1085
1086         if (CtdlAccessCheck(ac_logged_in)) return;
1087
1088         cprintf("%d inbox rules for %s\n", LISTING_FOLLOWS, CC->user.fullname);
1089
1090         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
1091         if (msg != NULL) {
1092                 if (!CM_IsEmpty(msg, eMesageText)) {
1093                         char *token; 
1094                         char *rest = msg->cm_fields[eMesageText];
1095                         while ((token = strtok_r(rest, "\n", &rest))) {
1096
1097                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1098                                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
1099                                         strcpy(token, "rule|"); 
1100                                         strcpy(&token[5], &token[14]);
1101                                 }
1102
1103                                 // Output only lines containing rules.
1104                                 if (!strncasecmp(token, "rule|", 5)) {
1105                                         cprintf("%s\n", token); 
1106                                 }
1107                         }
1108                 }
1109                 CM_Free(msg);
1110         }
1111         cprintf("000\n");
1112 }
1113
1114
1115 /*
1116  * Put InBox Rules
1117  *
1118  * User transmits the new inbox rules for the account.  They are inserted into the account, replacing the ones already there.
1119  */
1120 void cmd_pibr(char *argbuf) {
1121         if (CtdlAccessCheck(ac_logged_in)) return;
1122
1123         unbuffer_output();
1124         cprintf("%d send new rules\n", SEND_LISTING);
1125         char *newrules = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1126         StrBuf *NewConfig = NewStrBufPlain("Content-type: application/x-citadel-sieve-config; charset=UTF-8\nContent-transfer-encoding: 8bit\n\n", -1);
1127
1128         char *token; 
1129         char *rest = newrules;
1130         while ((token = strtok_r(rest, "\n", &rest))) {
1131                 // Accept only lines containing rules
1132                 if (!strncasecmp(token, "rule|", 5)) {
1133                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1134                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1135                 }
1136         }
1137         free(newrules);
1138
1139         // Fetch the existing config so we can merge in anything that is NOT a rule 
1140         // (Does not start with "rule|" but has at least one vertical bar)
1141         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
1142         if (msg != NULL) {
1143                 if (!CM_IsEmpty(msg, eMesageText)) {
1144                         rest = msg->cm_fields[eMesageText];
1145                         while ((token = strtok_r(rest, "\n", &rest))) {
1146                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1147                                 if ((strncasecmp(token, "# WEBCIT_RULE|", 14)) && (strncasecmp(token, "rule|", 5)) && (haschar(token, '|'))) {
1148                                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1149                                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1150                                 }
1151                         }
1152                 }
1153                 CM_Free(msg);
1154         }
1155
1156         /* we have composed the new configuration , now save it */
1157         long old_msgnum = CC->user.msgnum_inboxrules;
1158         char userconfigroomname[ROOMNAMELEN];
1159         CtdlMailboxName(userconfigroomname, sizeof userconfigroomname, &CC->user, USERCONFIGROOM);
1160         long new_msgnum = quickie_message("Citadel", NULL, NULL, userconfigroomname, ChrPtr(NewConfig), FMT_RFC822, "inbox rules configuration");
1161         FreeStrBuf(&NewConfig);
1162         CtdlGetUserLock(&CC->user, CC->curr_user);
1163         CC->user.msgnum_inboxrules = new_msgnum;
1164         CtdlPutUserLock(&CC->user);
1165         if (old_msgnum > 0) {
1166                 syslog(LOG_DEBUG, "Deleting old message %ld from %s", old_msgnum, userconfigroomname);
1167                 CtdlDeleteMessages(userconfigroomname, &old_msgnum, 1, "");
1168         }
1169 }
1170
1171
1172 CTDL_MODULE_INIT(sieve)
1173 {
1174         if (!threading)
1175         {
1176                 CtdlRegisterProtoHook(cmd_gibr, "GIBR", "Get InBox Rules");
1177                 CtdlRegisterProtoHook(cmd_pibr, "PIBR", "Put InBox Rules");
1178                 CtdlRegisterRoomHook(serv_inboxrules_roomhook);
1179                 CtdlRegisterSessionHook(perform_inbox_processing, EVT_HOUSE, PRIO_HOUSE + 10);
1180         }
1181         
1182         /* return our module name for the log */
1183         return "inboxrules";
1184 }