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