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