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