Implemented keep_message flag to determine whether to keep or delete the copy of...
[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 keep_message = 1;                   // Nonzero to keep the message in the inbox after processing, 0 to delete it.
754         int i;
755
756         syslog(LOG_DEBUG, "inboxrules: processing message #%ld which is higher than %ld, we are in %s", msgnum, ii->lastproc, CC->room.QRname);
757
758         if (ii->num_rules <= 0) {
759                 syslog(LOG_DEBUG, "inboxrules: rule set is empty");
760                 return;
761         }
762
763         for (i=0; i<ii->num_rules; ++i) {
764                 syslog(LOG_DEBUG, "inboxrules: processing rule %d is %s", i, field_keys[ ii->rules[i].compared_field ]);
765                 rule_activated = 0;
766
767                 // Before doing a field compare, check to see if we have the correct parts of the message in memory.
768
769                 switch(ii->rules[i].compared_field) {
770                         // These fields require loading only the top-level headers
771                         case field_from:                // From:
772                         case field_tocc:                // To: or Cc:
773                         case field_subject:             // Subject:
774                         case field_replyto:             // Reply-to:
775                         case field_listid:              // List-ID:
776                         case field_envto:               // Envelope-to:
777                         case field_envfrom:             // Return-path:
778                                 if (!headers_loaded) {
779                                         syslog(LOG_DEBUG, "inboxrules: loading headers for message %ld", msgnum);
780                                         msg = CtdlFetchMessage(msgnum, 0);
781                                         if (!msg) {
782                                                 return;
783                                         }
784                                         headers_loaded = 1;
785                                 }
786                                 break;
787                         // These fields are not stored as Citadel headers, and therefore require a full message load.
788                         case field_sender:
789                         case field_resentfrom:
790                         case field_resentto:
791                         case field_xmailer:
792                         case field_xspamflag:
793                         case field_xspamstatus:
794                                 if (!body_loaded) {
795                                         syslog(LOG_DEBUG, "inboxrules: loading all of message %ld", msgnum);
796                                         if (msg != NULL) {
797                                                 CM_Free(msg);
798                                         }
799                                         msg = CtdlFetchMessage(msgnum, 1);
800                                         if (!msg) {
801                                                 return;
802                                         }
803                                         headers_loaded = 1;
804                                         body_loaded = 1;
805                                 }
806                                 break;
807                         case field_size:
808                                 if (!metadata_loaded) {
809                                         syslog(LOG_DEBUG, "inboxrules: loading metadata for message %ld", msgnum);
810                                         GetMetaData(&smi, msgnum);
811                                         metadata_loaded = 1;
812                                 }
813                                 break;
814                         case field_all:
815                                 syslog(LOG_DEBUG, "inboxrules: this is an always-on rule");
816                                 break;
817                         default:
818                                 syslog(LOG_DEBUG, "inboxrules: unknown rule key");
819                 }
820
821                 // If the rule involves a field comparison, load the field to be compared.
822                 compare_me[0] = 0;
823                 switch(ii->rules[i].compared_field) {
824
825                         case field_from:                // From:
826                                 if (!IsEmptyStr(msg->cm_fields[erFc822Addr])) {
827                                         safestrncpy(compare_me, msg->cm_fields[erFc822Addr], sizeof compare_me);
828                                 }
829                                 break;
830                         case field_tocc:                // To: or Cc:
831                                 if (!IsEmptyStr(msg->cm_fields[eRecipient])) {
832                                         safestrncpy(compare_me, msg->cm_fields[eRecipient], sizeof compare_me);
833                                 }
834                                 if (!IsEmptyStr(msg->cm_fields[eCarbonCopY])) {
835                                         if (!IsEmptyStr(compare_me)) {
836                                                 strcat(compare_me, ",");
837                                         }
838                                         safestrncpy(&compare_me[strlen(compare_me)], msg->cm_fields[eCarbonCopY], (sizeof compare_me - strlen(compare_me)));
839                                 }
840                                 break;
841                         case field_subject:             // Subject:
842                                 if (!IsEmptyStr(msg->cm_fields[eMsgSubject])) {
843                                         safestrncpy(compare_me, msg->cm_fields[eMsgSubject], sizeof compare_me);
844                                 }
845                                 break;
846                         case field_replyto:             // Reply-to:
847                                 if (!IsEmptyStr(msg->cm_fields[eReplyTo])) {
848                                         safestrncpy(compare_me, msg->cm_fields[eReplyTo], sizeof compare_me);
849                                 }
850                                 break;
851                         case field_listid:              // List-ID:
852                                 if (!IsEmptyStr(msg->cm_fields[eListID])) {
853                                         safestrncpy(compare_me, msg->cm_fields[eListID], sizeof compare_me);
854                                 }
855                                 break;
856                         case field_envto:               // Envelope-to:
857                                 if (!IsEmptyStr(msg->cm_fields[eenVelopeTo])) {
858                                         safestrncpy(compare_me, msg->cm_fields[eenVelopeTo], sizeof compare_me);
859                                 }
860                                 break;
861                         case field_envfrom:             // Return-path:
862                                 if (!IsEmptyStr(msg->cm_fields[eMessagePath])) {
863                                         safestrncpy(compare_me, msg->cm_fields[eMessagePath], sizeof compare_me);
864                                 }
865                                 break;
866
867                         case field_sender:
868                         case field_resentfrom:
869                         case field_resentto:
870                         case field_xmailer:
871                         case field_xspamflag:
872                         case field_xspamstatus:
873
874                         default:
875                                 break;
876                 }
877
878                 // Message data to compare is loaded, now do something.
879                 switch(ii->rules[i].compared_field) {
880                         case field_from:                // From:
881                         case field_tocc:                // To: or Cc:
882                         case field_subject:             // Subject:
883                         case field_replyto:             // Reply-to:
884                         case field_listid:              // List-ID:
885                         case field_envto:               // Envelope-to:
886                         case field_envfrom:             // Return-path:
887                         case field_sender:
888                         case field_resentfrom:
889                         case field_resentto:
890                         case field_xmailer:
891                         case field_xspamflag:
892                         case field_xspamstatus:
893
894                                 // For all of the above fields, we can compare the field we've loaded into the buffer.
895                                 syslog(LOG_DEBUG, "Value of field to compare is: <%s>", compare_me);
896                                 switch(ii->rules[i].field_compare_op) {
897                                         case fcomp_contains:
898                                         case fcomp_matches:
899                                                 rule_activated = (bmstrcasestr(compare_me, ii->rules[i].compared_value) ? 1 : 0);
900                                                 syslog(LOG_DEBUG, "Does %s contain %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
901                                                 break;
902                                         case fcomp_notcontains:
903                                         case fcomp_notmatches:
904                                                 rule_activated = (bmstrcasestr(compare_me, ii->rules[i].compared_value) ? 0 : 1);
905                                                 syslog(LOG_DEBUG, "Does %s contain %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
906                                                 break;
907                                         case fcomp_is:
908                                                 rule_activated = (strcasecmp(compare_me, ii->rules[i].compared_value) ? 0 : 1);
909                                                 syslog(LOG_DEBUG, "Does %s equal %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
910                                                 break;
911                                         case fcomp_isnot:
912                                                 rule_activated = (strcasecmp(compare_me, ii->rules[i].compared_value) ? 1 : 0);
913                                                 syslog(LOG_DEBUG, "Does %s equal %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
914                                                 break;
915                                 }
916                                 break;
917
918                         case field_size:
919                                 rule_activated = 0;
920                                 syslog(LOG_DEBUG, "comparing actual message size %ld to rule message size %ld", smi.meta_rfc822_length, ii->rules[i].compared_size);
921                                 switch(ii->rules[i].field_compare_op) {
922                                         case scomp_larger:
923                                                 rule_activated = ((smi.meta_rfc822_length > ii->rules[i].compared_size) ? 1 : 0);
924                                                 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");
925                                                 break;
926                                         case scomp_smaller:
927                                                 rule_activated = ((smi.meta_rfc822_length < ii->rules[i].compared_size) ? 1 : 0);
928                                                 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");
929                                                 break;
930                                 }
931                                 break;
932                         case field_all:                 // The "all messages" rule ALWAYS triggers
933                                 rule_activated = 1;
934                                 break;
935                         default:                        // no matches, fall through and do nothing
936                                 syslog(LOG_DEBUG, "inboxrules: an unknown field comparison was encountered");
937                                 rule_activated = 0;
938                                 break;
939                 }
940
941                 // If the rule matched, perform the requested action.
942                 if (rule_activated) {
943                         syslog(LOG_DEBUG, "\033[32m\033[7mrule activated\033[0m");              // FIXME remove color
944
945                         // Perform the requested action. FIXME write these
946                         switch(ii->rules[i].action) {
947                                 case action_keep:
948                                         keep_message = 1;
949                                         break;
950                                 case action_discard:
951                                         keep_message = 0;
952                                         break;
953                                 case action_reject:
954                                         // FIXME send the reject message
955                                         keep_message = 0;
956                                         break;
957                                 case action_fileinto:
958                                         // FIXME put it in the other room
959                                         keep_message = 0;
960                                         break;
961                                 case action_redirect:
962                                         // FIXME send it to the recipient
963                                         keep_message = 0;
964                                         break;
965                                 case action_vacation:
966                                         // FIXME send the vacation message
967                                         keep_message = 1;
968                                         break;
969                         }
970
971                         // Now perform the "final" action (anything other than "stop" means continue)
972                         if (ii->rules[i].final_action == final_stop) {
973                                 syslog(LOG_DEBUG, "\033[33m\033[7mSTOP\033[0m");                // FIXME remove color
974                                 i = ii->num_rules + 1;                                          // throw us out of scope to stop
975                         }
976
977
978                 }
979                 else {
980                         syslog(LOG_DEBUG, "\033[31m\033[7mrule not activated\033[0m");          // FIXME remove color
981                 }
982         }
983
984         if (msg != NULL) {              // Delete the copy of the message that is currently in memory.  We don't need it anymore.
985                 CM_Free(msg);
986         }
987
988         if (!keep_message) {            // Delete the copy of the message that is currently in the inbox, if rules dictated that.
989                 // FIXME delete the message
990                 syslog(LOG_DEBUG, "\033[36m\033[7mDELETE FROM INBOX\033[0m");
991         }
992
993         ii->lastproc = msgnum;          // make note of the last message we processed, so we don't scan the whole inbox again
994 }
995
996
997 /*
998  * A user account is identified as requring inbox processing.
999  * Do it.
1000  */
1001 void do_inbox_processing_for_user(long usernum) {
1002         struct CtdlMessage *msg;
1003         struct inboxrules *ii;
1004         char roomname[ROOMNAMELEN];
1005
1006         if (CtdlGetUserByNumber(&CC->user, usernum) == 0) {
1007                 if (CC->user.msgnum_inboxrules <= 0) {
1008                         return;                                         // this user has no inbox rules
1009                 }
1010
1011                 msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
1012                 if (msg == NULL) {
1013                         return;                                         // config msgnum is set but that message does not exist
1014                 }
1015         
1016                 ii = deserialize_inbox_rules(msg->cm_fields[eMesageText]);
1017                 CM_Free(msg);
1018         
1019                 if (ii == NULL) {
1020                         return;                                         // config message exists but body is null
1021                 }
1022
1023                 syslog(LOG_DEBUG, "inboxrules: for %s", CC->user.fullname);
1024
1025                 // Go to the user's inbox room and process all new messages
1026                 snprintf(roomname, sizeof roomname, "%010ld.%s", usernum, MAILROOM);
1027                 if (CtdlGetRoom(&CC->room, roomname) == 0) {
1028                         CtdlForEachMessage(MSGS_GT, ii->lastproc, NULL, NULL, NULL, inbox_do_msg, (void *) ii);
1029                 }
1030
1031                 // FIXME reserialize our inbox rules/state and write changes back to the config room
1032                 free_inbox_rules(ii);
1033         }
1034 }
1035
1036
1037 /*
1038  * Here is an array of users (by number) who have received messages in their inbox and may require processing.
1039  */
1040 long *users_requiring_inbox_processing = NULL;
1041 int num_urip = 0;
1042 int num_urip_alloc = 0;
1043
1044
1045 /*
1046  * Perform inbox processing for all rooms which require it
1047  */
1048 void perform_inbox_processing(void) {
1049         if (num_urip == 0) {
1050                 return;                                                                                 // no action required
1051         }
1052
1053         for (int i=0; i<num_urip; ++i) {
1054                 do_inbox_processing_for_user(users_requiring_inbox_processing[i]);
1055         }
1056
1057         free(users_requiring_inbox_processing);
1058         users_requiring_inbox_processing = NULL;
1059         num_urip = 0;
1060         num_urip_alloc = 0;
1061 }
1062
1063
1064 /*
1065  * This function is called after a message is saved to a room.
1066  * If it's someone's inbox, we have to check for inbox rules
1067  */
1068 int serv_inboxrules_roomhook(struct ctdlroom *room) {
1069
1070         // Is this someone's inbox?
1071         if (!strcasecmp(&room->QRname[11], MAILROOM)) {
1072                 long usernum = atol(room->QRname);
1073                 if (usernum > 0) {
1074
1075                         // first check to see if this user is already on the list
1076                         if (num_urip > 0) {
1077                                 for (int i=0; i<=num_urip; ++i) {
1078                                         if (users_requiring_inbox_processing[i] == usernum) {           // already on the list!
1079                                                 return(0);
1080                                         }
1081                                 }
1082                         }
1083
1084                         // make room if we need to
1085                         if (num_urip_alloc == 0) {
1086                                 num_urip_alloc = 100;
1087                                 users_requiring_inbox_processing = malloc(sizeof(long) * num_urip_alloc);
1088                         }
1089                         else if (num_urip >= num_urip_alloc) {
1090                                 num_urip_alloc += 100;
1091                                 users_requiring_inbox_processing = realloc(users_requiring_inbox_processing, (sizeof(long) * num_urip_alloc));
1092                         }
1093                         
1094                         // now add the user to the list
1095                         users_requiring_inbox_processing[num_urip++] = usernum;
1096                 }
1097         }
1098
1099         // No errors are possible from this function.
1100         return(0);
1101 }
1102
1103
1104
1105 /*
1106  * Get InBox Rules
1107  *
1108  * This is a client-facing function which fetches the user's inbox rules -- it omits all lines containing anything other than a rule.
1109  * 
1110  * hmmmmm ... should we try to rebuild this in terms of deserialize_inbox_rules() instread?
1111  */
1112 void cmd_gibr(char *argbuf) {
1113
1114         if (CtdlAccessCheck(ac_logged_in)) return;
1115
1116         cprintf("%d inbox rules for %s\n", LISTING_FOLLOWS, CC->user.fullname);
1117
1118         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
1119         if (msg != NULL) {
1120                 if (!CM_IsEmpty(msg, eMesageText)) {
1121                         char *token; 
1122                         char *rest = msg->cm_fields[eMesageText];
1123                         while ((token = strtok_r(rest, "\n", &rest))) {
1124
1125                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1126                                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
1127                                         strcpy(token, "rule|"); 
1128                                         strcpy(&token[5], &token[14]);
1129                                 }
1130
1131                                 // Output only lines containing rules.
1132                                 if (!strncasecmp(token, "rule|", 5)) {
1133                                         cprintf("%s\n", token); 
1134                                 }
1135                         }
1136                 }
1137                 CM_Free(msg);
1138         }
1139         cprintf("000\n");
1140 }
1141
1142
1143 /*
1144  * Put InBox Rules
1145  *
1146  * User transmits the new inbox rules for the account.  They are inserted into the account, replacing the ones already there.
1147  */
1148 void cmd_pibr(char *argbuf) {
1149         if (CtdlAccessCheck(ac_logged_in)) return;
1150
1151         unbuffer_output();
1152         cprintf("%d send new rules\n", SEND_LISTING);
1153         char *newrules = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1154         StrBuf *NewConfig = NewStrBufPlain("Content-type: application/x-citadel-sieve-config; charset=UTF-8\nContent-transfer-encoding: 8bit\n\n", -1);
1155
1156         char *token; 
1157         char *rest = newrules;
1158         while ((token = strtok_r(rest, "\n", &rest))) {
1159                 // Accept only lines containing rules
1160                 if (!strncasecmp(token, "rule|", 5)) {
1161                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1162                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1163                 }
1164         }
1165         free(newrules);
1166
1167         // Fetch the existing config so we can merge in anything that is NOT a rule 
1168         // (Does not start with "rule|" but has at least one vertical bar)
1169         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
1170         if (msg != NULL) {
1171                 if (!CM_IsEmpty(msg, eMesageText)) {
1172                         rest = msg->cm_fields[eMesageText];
1173                         while ((token = strtok_r(rest, "\n", &rest))) {
1174                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
1175                                 if ((strncasecmp(token, "# WEBCIT_RULE|", 14)) && (strncasecmp(token, "rule|", 5)) && (haschar(token, '|'))) {
1176                                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1177                                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1178                                 }
1179                         }
1180                 }
1181                 CM_Free(msg);
1182         }
1183
1184         /* we have composed the new configuration , now save it */
1185         long old_msgnum = CC->user.msgnum_inboxrules;
1186         char userconfigroomname[ROOMNAMELEN];
1187         CtdlMailboxName(userconfigroomname, sizeof userconfigroomname, &CC->user, USERCONFIGROOM);
1188         long new_msgnum = quickie_message("Citadel", NULL, NULL, userconfigroomname, ChrPtr(NewConfig), FMT_RFC822, "inbox rules configuration");
1189         FreeStrBuf(&NewConfig);
1190         CtdlGetUserLock(&CC->user, CC->curr_user);
1191         CC->user.msgnum_inboxrules = new_msgnum;
1192         CtdlPutUserLock(&CC->user);
1193         if (old_msgnum > 0) {
1194                 syslog(LOG_DEBUG, "Deleting old message %ld from %s", old_msgnum, userconfigroomname);
1195                 CtdlDeleteMessages(userconfigroomname, &old_msgnum, 1, "");
1196         }
1197 }
1198
1199
1200 CTDL_MODULE_INIT(sieve)
1201 {
1202         if (!threading)
1203         {
1204                 CtdlRegisterProtoHook(cmd_gibr, "GIBR", "Get InBox Rules");
1205                 CtdlRegisterProtoHook(cmd_pibr, "PIBR", "Put InBox Rules");
1206                 CtdlRegisterRoomHook(serv_inboxrules_roomhook);
1207                 CtdlRegisterSessionHook(perform_inbox_processing, EVT_HOUSE, PRIO_HOUSE + 10);
1208         }
1209         
1210         /* return our module name for the log */
1211         return "inboxrules";
1212 }