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