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