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