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