Record the 'last message processed' in the user record instead of in the inbox config...
[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                 // This is a legacy location for this value and will only be used if it's the only one present.
383                 else if (!strncasecmp(token, "lastproc|", 5)) {
384                         ibr->lastproc = atol(&token[9]);
385                 }
386
387                 // Lines which do not contain a recognizable token must be IGNORED.  These lines may be left over
388                 // from a previous version and will disappear when we rewrite the config.
389
390         }
391
392         free(sr);               // free our copy of the source buffer that has now been trashed with null bytes...
393         return(ibr);            // and return our complex data type to the caller.
394 }
395
396
397 // Perform the "fileinto" action (save the message in another room)
398 // Returns: 1 or 0 to tell the caller to keep (1) or delete (0) the inbox copy of the message.
399 //
400 int inbox_do_fileinto(struct irule *rule, long msgnum) {
401         char *dest_folder = rule->file_into;
402         char original_room_name[ROOMNAMELEN];
403         char foldername[ROOMNAMELEN];
404         int c;
405
406         // Situations where we want to just keep the message in the inbox:
407         if (
408                 (IsEmptyStr(dest_folder))                       // no destination room was specified
409                 || (!strcasecmp(dest_folder, "INBOX"))          // fileinto inbox is the same as keep
410                 || (!strcasecmp(dest_folder, MAILROOM))         // fileinto "Mail" is the same as keep
411         ) {
412                 return(1);                                      // don't delete the inbox copy if this failed
413         }
414
415         // Remember what room we came from
416         safestrncpy(original_room_name, CC->room.QRname, sizeof original_room_name);
417
418         // First try a mailbox name match (check personal mail folders first)
419         strcpy(foldername, original_room_name);                                 // This keeps the user namespace of the inbox
420         snprintf(&foldername[10], sizeof(foldername)-10, ".%s", dest_folder);   // And this tacks on the target room name
421         c = CtdlGetRoom(&CC->room, foldername);
422
423         // Then a regular room name match (public and private rooms)
424         if (c != 0) {
425                 safestrncpy(foldername, dest_folder, sizeof foldername);
426                 c = CtdlGetRoom(&CC->room, foldername);
427         }
428
429         if (c != 0) {
430                 syslog(LOG_WARNING, "inboxrules: target <%s> does not exist", dest_folder);
431                 return(1);                                      // don't delete the inbox copy if this failed
432         }
433
434         // Yes, we actually have to go there
435         CtdlUserGoto(NULL, 0, 0, NULL, NULL, NULL, NULL);
436
437         c = CtdlSaveMsgPointersInRoom(NULL, &msgnum, 1, 0, NULL, 0);
438
439         // Go back to the room we came from
440         if (strcasecmp(original_room_name, CC->room.QRname)) {
441                 CtdlUserGoto(original_room_name, 0, 0, NULL, NULL, NULL, NULL);
442         }
443
444         return(0);                                              // delete the inbox copy
445 }
446
447
448 // Perform the "redirect" action (divert the message to another email address)
449 // Returns: 1 or 0 to tell the caller to keep (1) or delete (0) the inbox copy of the message.
450 //
451 int inbox_do_redirect(struct irule *rule, long msgnum) {
452         if (IsEmptyStr(rule->redirect_to)) {
453                 syslog(LOG_WARNING, "inboxrules: inbox_do_redirect() invalid recipient <%s>", rule->redirect_to);
454                 return(1);                                      // don't delete the inbox copy if this failed
455         }
456
457         recptypes *valid = validate_recipients(rule->redirect_to, NULL, 0);
458         if (valid == NULL) {
459                 syslog(LOG_WARNING, "inboxrules: inbox_do_redirect() invalid recipient <%s>", rule->redirect_to);
460                 return(1);                                      // don't delete the inbox copy if this failed
461         }
462         if (valid->num_error > 0) {
463                 free_recipients(valid);
464                 syslog(LOG_WARNING, "inboxrules: inbox_do_redirect() invalid recipient <%s>", rule->redirect_to);
465                 return(1);                                      // don't delete the inbox copy if this failed
466         }
467
468         struct CtdlMessage *msg = CtdlFetchMessage(msgnum, 1);
469         if (msg == NULL) {
470                 free_recipients(valid);
471                 syslog(LOG_WARNING, "inboxrules: cannot reload message %ld for forwarding", msgnum);
472                 return(1);                                      // don't delete the inbox copy if this failed
473         }
474
475         CtdlSubmitMsg(msg, valid, NULL, 0);                     // send the message to the new recipient
476         free_recipients(valid);
477         CM_Free(msg);
478         return(0);                                              // delete the inbox copy
479 }
480
481
482 // Perform the "reject" action (delete the message, and tell the sender we deleted it)
483 //
484 void inbox_do_reject(struct irule *rule, struct CtdlMessage *msg) {
485         syslog(LOG_DEBUG, "inbox_do_reject: sender: <%s>, reject message: <%s>",
486                 msg->cm_fields[erFc822Addr],
487                 rule->autoreply_message
488         );
489
490         // If we can't determine who sent the message, reject silently.
491         char *sender;
492         if (!IsEmptyStr(msg->cm_fields[eMessagePath])) {
493                 sender = msg->cm_fields[eMessagePath];
494         }
495         else if (!IsEmptyStr(msg->cm_fields[erFc822Addr])) {
496                 sender = msg->cm_fields[erFc822Addr];
497         }
498         else {
499                 return;
500         }
501
502         // Assemble the reject message.
503         char *reject_text = malloc(strlen(rule->autoreply_message) + 1024);
504         if (reject_text == NULL) {
505                 return;
506         }
507         sprintf(reject_text, 
508                 "Content-type: text/plain\n"
509                 "\n"
510                 "The message was refused by the recipient's mail filtering program.\n"
511                 "The reason given was as follows:\n"
512                 "\n"
513                 "%s\n"
514                 "\n"
515         ,
516                 rule->autoreply_message
517         );
518
519         // Deliver the message
520         quickie_message(
521                 NULL,
522                 msg->cm_fields[eenVelopeTo],
523                 sender,
524                 NULL,
525                 reject_text,
526                 FMT_RFC822,
527                 "Delivery status notification"
528         );
529         free(reject_text);
530 }
531
532
533 /*
534  * Process a single message.  We know the room, the user, the rules, the message number, etc.
535  */
536 void inbox_do_msg(long msgnum, void *userdata) {
537         struct inboxrules *ii = (struct inboxrules *) userdata;
538         struct CtdlMessage *msg = NULL;         // If we are loading a message to process, put it here.
539         int headers_loaded = 0;                 // Did we load the headers yet?  Do it only once.
540         int body_loaded = 0;                    // Did we load the message body yet?  Do it only once.
541         int metadata_loaded = 0;                // Did we load the metadata yet?  Do it only once.
542         struct MetaData smi;                    // If we are loading the metadata to compare, put it here.
543         int rule_activated = 0;                 // On each rule, this is set if the compare succeeds and the rule activates.
544         char compare_me[SIZ];                   // On each rule, we will store the field to be compared here.
545         int keep_message = 1;                   // Nonzero to keep the message in the inbox after processing, 0 to delete it.
546         int i;
547
548         syslog(LOG_DEBUG, "inboxrules: processing message #%ld which is higher than %ld, we are in %s", msgnum, ii->lastproc, CC->room.QRname);
549
550         if (ii->num_rules <= 0) {
551                 syslog(LOG_DEBUG, "inboxrules: rule set is empty");
552                 return;
553         }
554
555         for (i=0; i<ii->num_rules; ++i) {
556                 syslog(LOG_DEBUG, "inboxrules: processing rule %d is %s", i, field_keys[ ii->rules[i].compared_field ]);
557                 rule_activated = 0;
558
559                 // Before doing a field compare, check to see if we have the correct parts of the message in memory.
560
561                 switch(ii->rules[i].compared_field) {
562                         // These fields require loading only the top-level headers
563                         case field_from:                // From:
564                         case field_tocc:                // To: or Cc:
565                         case field_subject:             // Subject:
566                         case field_replyto:             // Reply-to:
567                         case field_listid:              // List-ID:
568                         case field_envto:               // Envelope-to:
569                         case field_envfrom:             // Return-path:
570                                 if (!headers_loaded) {
571                                         syslog(LOG_DEBUG, "inboxrules: loading headers for message %ld", msgnum);
572                                         msg = CtdlFetchMessage(msgnum, 0);
573                                         if (!msg) {
574                                                 return;
575                                         }
576                                         headers_loaded = 1;
577                                 }
578                                 break;
579                         // These fields are not stored as Citadel headers, and therefore require a full message load.
580                         case field_sender:
581                         case field_resentfrom:
582                         case field_resentto:
583                         case field_xmailer:
584                         case field_xspamflag:
585                         case field_xspamstatus:
586                                 if (!body_loaded) {
587                                         syslog(LOG_DEBUG, "inboxrules: loading all of message %ld", msgnum);
588                                         if (msg != NULL) {
589                                                 CM_Free(msg);
590                                         }
591                                         msg = CtdlFetchMessage(msgnum, 1);
592                                         if (!msg) {
593                                                 return;
594                                         }
595                                         headers_loaded = 1;
596                                         body_loaded = 1;
597                                 }
598                                 break;
599                         case field_size:
600                                 if (!metadata_loaded) {
601                                         syslog(LOG_DEBUG, "inboxrules: loading metadata for message %ld", msgnum);
602                                         GetMetaData(&smi, msgnum);
603                                         metadata_loaded = 1;
604                                 }
605                                 break;
606                         case field_all:
607                                 syslog(LOG_DEBUG, "inboxrules: this is an always-on rule");
608                                 break;
609                         default:
610                                 syslog(LOG_DEBUG, "inboxrules: unknown rule key");
611                 }
612
613                 // If the rule involves a field comparison, load the field to be compared.
614                 compare_me[0] = 0;
615                 switch(ii->rules[i].compared_field) {
616
617                         case field_from:                // From:
618                                 if (!IsEmptyStr(msg->cm_fields[erFc822Addr])) {
619                                         safestrncpy(compare_me, msg->cm_fields[erFc822Addr], sizeof compare_me);
620                                 }
621                                 break;
622                         case field_tocc:                // To: or Cc:
623                                 if (!IsEmptyStr(msg->cm_fields[eRecipient])) {
624                                         safestrncpy(compare_me, msg->cm_fields[eRecipient], sizeof compare_me);
625                                 }
626                                 if (!IsEmptyStr(msg->cm_fields[eCarbonCopY])) {
627                                         if (!IsEmptyStr(compare_me)) {
628                                                 strcat(compare_me, ",");
629                                         }
630                                         safestrncpy(&compare_me[strlen(compare_me)], msg->cm_fields[eCarbonCopY], (sizeof compare_me - strlen(compare_me)));
631                                 }
632                                 break;
633                         case field_subject:             // Subject:
634                                 if (!IsEmptyStr(msg->cm_fields[eMsgSubject])) {
635                                         safestrncpy(compare_me, msg->cm_fields[eMsgSubject], sizeof compare_me);
636                                 }
637                                 break;
638                         case field_replyto:             // Reply-to:
639                                 if (!IsEmptyStr(msg->cm_fields[eReplyTo])) {
640                                         safestrncpy(compare_me, msg->cm_fields[eReplyTo], sizeof compare_me);
641                                 }
642                                 break;
643                         case field_listid:              // List-ID:
644                                 if (!IsEmptyStr(msg->cm_fields[eListID])) {
645                                         safestrncpy(compare_me, msg->cm_fields[eListID], sizeof compare_me);
646                                 }
647                                 break;
648                         case field_envto:               // Envelope-to:
649                                 if (!IsEmptyStr(msg->cm_fields[eenVelopeTo])) {
650                                         safestrncpy(compare_me, msg->cm_fields[eenVelopeTo], sizeof compare_me);
651                                 }
652                                 break;
653                         case field_envfrom:             // Return-path:
654                                 if (!IsEmptyStr(msg->cm_fields[eMessagePath])) {
655                                         safestrncpy(compare_me, msg->cm_fields[eMessagePath], sizeof compare_me);
656                                 }
657                                 break;
658
659                         case field_sender:
660                         case field_resentfrom:
661                         case field_resentto:
662                         case field_xmailer:
663                         case field_xspamflag:
664                         case field_xspamstatus:
665
666                         default:
667                                 break;
668                 }
669
670                 // Message data to compare is loaded, now do something.
671                 switch(ii->rules[i].compared_field) {
672                         case field_from:                // From:
673                         case field_tocc:                // To: or Cc:
674                         case field_subject:             // Subject:
675                         case field_replyto:             // Reply-to:
676                         case field_listid:              // List-ID:
677                         case field_envto:               // Envelope-to:
678                         case field_envfrom:             // Return-path:
679                         case field_sender:
680                         case field_resentfrom:
681                         case field_resentto:
682                         case field_xmailer:
683                         case field_xspamflag:
684                         case field_xspamstatus:
685
686                                 // For all of the above fields, we can compare the field we've loaded into the buffer.
687                                 syslog(LOG_DEBUG, "Value of field to compare is: <%s>", compare_me);
688                                 switch(ii->rules[i].field_compare_op) {
689                                         case fcomp_contains:
690                                         case fcomp_matches:
691                                                 rule_activated = (bmstrcasestr(compare_me, ii->rules[i].compared_value) ? 1 : 0);
692                                                 syslog(LOG_DEBUG, "Does %s contain %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
693                                                 break;
694                                         case fcomp_notcontains:
695                                         case fcomp_notmatches:
696                                                 rule_activated = (bmstrcasestr(compare_me, ii->rules[i].compared_value) ? 0 : 1);
697                                                 syslog(LOG_DEBUG, "Does %s contain %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
698                                                 break;
699                                         case fcomp_is:
700                                                 rule_activated = (strcasecmp(compare_me, ii->rules[i].compared_value) ? 0 : 1);
701                                                 syslog(LOG_DEBUG, "Does %s equal %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
702                                                 break;
703                                         case fcomp_isnot:
704                                                 rule_activated = (strcasecmp(compare_me, ii->rules[i].compared_value) ? 1 : 0);
705                                                 syslog(LOG_DEBUG, "Does %s equal %s? %s", compare_me, ii->rules[i].compared_value, rule_activated?"yes":"no");
706                                                 break;
707                                 }
708                                 break;
709
710                         case field_size:
711                                 rule_activated = 0;
712                                 syslog(LOG_DEBUG, "comparing actual message size %ld to rule message size %ld", smi.meta_rfc822_length, ii->rules[i].compared_size);
713                                 switch(ii->rules[i].field_compare_op) {
714                                         case scomp_larger:
715                                                 rule_activated = ((smi.meta_rfc822_length > ii->rules[i].compared_size) ? 1 : 0);
716                                                 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");
717                                                 break;
718                                         case scomp_smaller:
719                                                 rule_activated = ((smi.meta_rfc822_length < ii->rules[i].compared_size) ? 1 : 0);
720                                                 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");
721                                                 break;
722                                 }
723                                 break;
724                         case field_all:                 // The "all messages" rule ALWAYS triggers
725                                 rule_activated = 1;
726                                 break;
727                         default:                        // no matches, fall through and do nothing
728                                 syslog(LOG_DEBUG, "inboxrules: an unknown field comparison was encountered");
729                                 rule_activated = 0;
730                                 break;
731                 }
732
733                 // If the rule matched, perform the requested action.
734                 if (rule_activated) {
735                         syslog(LOG_DEBUG, "inboxrules: rule activated");
736
737                         // Perform the requested action
738                         switch(ii->rules[i].action) {
739                                 case action_keep:
740                                         keep_message = 1;
741                                         break;
742                                 case action_discard:
743                                         keep_message = 0;
744                                         break;
745                                 case action_reject:
746                                         inbox_do_reject(&ii->rules[i], msg);
747                                         keep_message = 0;
748                                         break;
749                                 case action_fileinto:
750                                         keep_message = inbox_do_fileinto(&ii->rules[i], msgnum);
751                                         break;
752                                 case action_redirect:
753                                         keep_message = inbox_do_redirect(&ii->rules[i], msgnum);
754                                         break;
755                                 case action_vacation:
756                                         // inbox_do_vacation(&ii->rules[i], msg);
757                                         keep_message = 1;
758                                         break;
759                         }
760
761                         // Now perform the "final" action (anything other than "stop" means continue)
762                         if (ii->rules[i].final_action == final_stop) {
763                                 syslog(LOG_DEBUG, "inboxrules: stop processing");
764                                 i = ii->num_rules + 1;                                  // throw us out of scope to stop
765                         }
766
767
768                 }
769                 else {
770                         syslog(LOG_DEBUG, "inboxrules: rule not activated");
771                 }
772         }
773
774         if (msg != NULL) {              // Delete the copy of the message that is currently in memory.  We don't need it anymore.
775                 CM_Free(msg);
776         }
777
778         if (!keep_message) {            // Delete the copy of the message that is currently in the inbox, if rules dictated that.
779                 syslog(LOG_DEBUG, "inboxrules: delete %ld from inbox", msgnum);
780                 CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");                    // we're in the inbox already
781         }
782
783         ii->lastproc = msgnum;          // make note of the last message we processed, so we don't scan the whole inbox again
784 }
785
786
787 /*
788  * A user account is identified as requring inbox processing.
789  * Do it.
790  */
791 void do_inbox_processing_for_user(long usernum) {
792         struct CtdlMessage *msg;
793         struct inboxrules *ii;
794         char roomname[ROOMNAMELEN];
795         char username[64];
796
797         if (CtdlGetUserByNumber(&CC->user, usernum) != 0) {     // grab the user record
798                 return;                                         // and bail out if we were given an invalid user
799         }
800
801         strcpy(username, CC->user.fullname);                    // save the user name so we can fetch it later and lock it
802
803         if (CC->user.msgnum_inboxrules <= 0) {
804                 return;                                         // this user has no inbox rules
805         }
806
807         msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
808         if (msg == NULL) {
809                 return;                                         // config msgnum is set but that message does not exist
810         }
811
812         ii = deserialize_inbox_rules(msg->cm_fields[eMesageText]);
813         CM_Free(msg);
814
815         if (ii == NULL) {
816                 return;                                         // config message exists but body is null
817         }
818
819
820         syslog(LOG_DEBUG, "ii->lastproc                 %ld", ii->lastproc);
821         syslog(LOG_DEBUG, "CC->user.lastproc_inboxrules %ld", CC->user.lastproc_inboxrules);
822
823         if (ii->lastproc > CC->user.lastproc_inboxrules) {      // There might be a "last message processed" number left over
824                 CC->user.lastproc_inboxrules = ii->lastproc;    // in the ruleset from a previous version.  Use this if it is
825         }                                                       // a higher number.
826         else {
827                 ii->lastproc = CC->user.lastproc_inboxrules;
828         }
829
830         long original_lastproc = ii->lastproc;
831         syslog(LOG_DEBUG, "inboxrules: for %s, messages newer than %ld", CC->user.fullname, original_lastproc);
832
833         // Go to the user's inbox room and process all new messages
834         snprintf(roomname, sizeof roomname, "%010ld.%s", usernum, MAILROOM);
835         if (CtdlGetRoom(&CC->room, roomname) == 0) {
836                 CtdlForEachMessage(MSGS_GT, ii->lastproc, NULL, NULL, NULL, inbox_do_msg, (void *) ii);
837         }
838
839         // Record the number of the last message we processed
840         if (ii->lastproc > original_lastproc) {
841                 CtdlGetUserLock(&CC->user, username);
842                 CC->user.lastproc_inboxrules = ii->lastproc;    // Avoid processing the entire inbox next time
843                 CtdlPutUserLock(&CC->user);
844         }
845
846         // And free the memory.
847         free_inbox_rules(ii);
848 }
849
850
851 /*
852  * Here is an array of users (by number) who have received messages in their inbox and may require processing.
853  */
854 long *users_requiring_inbox_processing = NULL;
855 int num_urip = 0;
856 int num_urip_alloc = 0;
857
858
859 /*
860  * Perform inbox processing for all rooms which require it
861  */
862 void perform_inbox_processing(void) {
863         if (num_urip == 0) {
864                 return;                                                                                 // no action required
865         }
866
867         for (int i=0; i<num_urip; ++i) {
868                 do_inbox_processing_for_user(users_requiring_inbox_processing[i]);
869         }
870
871         free(users_requiring_inbox_processing);
872         users_requiring_inbox_processing = NULL;
873         num_urip = 0;
874         num_urip_alloc = 0;
875 }
876
877
878 /*
879  * This function is called after a message is saved to a room.
880  * If it's someone's inbox, we have to check for inbox rules
881  */
882 int serv_inboxrules_roomhook(struct ctdlroom *room) {
883
884         // Is this someone's inbox?
885         if (!strcasecmp(&room->QRname[11], MAILROOM)) {
886                 long usernum = atol(room->QRname);
887                 if (usernum > 0) {
888
889                         // first check to see if this user is already on the list
890                         if (num_urip > 0) {
891                                 for (int i=0; i<=num_urip; ++i) {
892                                         if (users_requiring_inbox_processing[i] == usernum) {           // already on the list!
893                                                 return(0);
894                                         }
895                                 }
896                         }
897
898                         // make room if we need to
899                         if (num_urip_alloc == 0) {
900                                 num_urip_alloc = 100;
901                                 users_requiring_inbox_processing = malloc(sizeof(long) * num_urip_alloc);
902                         }
903                         else if (num_urip >= num_urip_alloc) {
904                                 num_urip_alloc += 100;
905                                 users_requiring_inbox_processing = realloc(users_requiring_inbox_processing, (sizeof(long) * num_urip_alloc));
906                         }
907                         
908                         // now add the user to the list
909                         users_requiring_inbox_processing[num_urip++] = usernum;
910                 }
911         }
912
913         // No errors are possible from this function.
914         return(0);
915 }
916
917
918
919 /*
920  * Get InBox Rules
921  *
922  * This is a client-facing function which fetches the user's inbox rules -- it omits all lines containing anything other than a rule.
923  * 
924  * hmmmmm ... should we try to rebuild this in terms of deserialize_inbox_rules() instread?
925  */
926 void cmd_gibr(char *argbuf) {
927
928         if (CtdlAccessCheck(ac_logged_in)) return;
929
930         cprintf("%d inbox rules for %s\n", LISTING_FOLLOWS, CC->user.fullname);
931
932         struct CtdlMessage *msg = CtdlFetchMessage(CC->user.msgnum_inboxrules, 1);
933         if (msg != NULL) {
934                 if (!CM_IsEmpty(msg, eMesageText)) {
935                         char *token; 
936                         char *rest = msg->cm_fields[eMesageText];
937                         while ((token = strtok_r(rest, "\n", &rest))) {
938
939                                 // for backwards compatibility, "# WEBCIT_RULE" is an alias for "rule" 
940                                 if (!strncasecmp(token, "# WEBCIT_RULE|", 14)) {
941                                         strcpy(token, "rule|"); 
942                                         strcpy(&token[5], &token[14]);
943                                 }
944
945                                 // Output only lines containing rules.
946                                 if (!strncasecmp(token, "rule|", 5)) {
947                                         cprintf("%s\n", token); 
948                                 }
949                                 else {
950                                         cprintf("# invalid rule found : %s\n", token);
951                                 }
952                         }
953                 }
954                 CM_Free(msg);
955         }
956         cprintf("000\n");
957 }
958
959
960 /*
961  * Rewrite the rule set to disk after it has been modified
962  * Called by cmd_pibr()
963  * Returns the msgnum of the new rules message
964  */
965 void rewrite_rules_to_disk(const char *new_config) {
966         long old_msgnum = CC->user.msgnum_inboxrules;
967         char userconfigroomname[ROOMNAMELEN];
968         CtdlMailboxName(userconfigroomname, sizeof userconfigroomname, &CC->user, USERCONFIGROOM);
969         long new_msgnum = quickie_message("Citadel", NULL, NULL, userconfigroomname, new_config, FMT_RFC822, "inbox rules configuration");
970         CtdlGetUserLock(&CC->user, CC->curr_user);
971         CC->user.msgnum_inboxrules = new_msgnum;                // Now we know where to get the rules next time
972         CC->user.lastproc_inboxrules = new_msgnum;              // Avoid processing the entire inbox next time
973         CtdlPutUserLock(&CC->user);
974         if (old_msgnum > 0) {
975                 syslog(LOG_DEBUG, "Deleting old message %ld from %s", old_msgnum, userconfigroomname);
976                 CtdlDeleteMessages(userconfigroomname, &old_msgnum, 1, "");
977         }
978 }
979
980
981 /*
982  * Put InBox Rules
983  *
984  * User transmits the new inbox rules for the account.  They are inserted into the account, replacing the ones already there.
985  */
986 void cmd_pibr(char *argbuf) {
987         if (CtdlAccessCheck(ac_logged_in)) return;
988
989         unbuffer_output();
990         cprintf("%d send new rules\n", SEND_LISTING);
991         char *newrules = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
992         StrBuf *NewConfig = NewStrBufPlain("Content-type: application/x-citadel-sieve-config; charset=UTF-8\nContent-transfer-encoding: 8bit\n\n", -1);
993
994         char *token; 
995         char *rest = newrules;
996         while ((token = strtok_r(rest, "\n", &rest))) {
997                 // Accept only lines containing rules
998                 if (!strncasecmp(token, "rule|", 5)) {
999                         StrBufAppendBufPlain(NewConfig, token, -1, 0);
1000                         StrBufAppendBufPlain(NewConfig, HKEY("\n"), 0);
1001                 }
1002         }
1003         free(newrules);
1004         rewrite_rules_to_disk(ChrPtr(NewConfig));
1005         FreeStrBuf(&NewConfig);
1006 }
1007
1008
1009 CTDL_MODULE_INIT(sieve)
1010 {
1011         if (!threading)
1012         {
1013                 CtdlRegisterProtoHook(cmd_gibr, "GIBR", "Get InBox Rules");
1014                 CtdlRegisterProtoHook(cmd_pibr, "PIBR", "Put InBox Rules");
1015                 CtdlRegisterRoomHook(serv_inboxrules_roomhook);
1016                 CtdlRegisterSessionHook(perform_inbox_processing, EVT_HOUSE, PRIO_HOUSE + 10);
1017         }
1018         
1019         /* return our module name for the log */
1020         return "inboxrules";
1021 }