30e03100759b8272827a7792facf871325be1b96
[citadel.git] / citadel / modules / sieve / serv_sieve.c
1 /*
2  * This module glues libSieve to the Citadel server in order to implement
3  * the Sieve mailbox filtering language (RFC 3028).
4  *
5  * Copyright (c) 1987-2018 by the citadel.org team
6  *
7  * This program is open source software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License version 3.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  */
15
16 #include "sysdep.h"
17 #include <stdlib.h>
18 #include <unistd.h>
19 #include <stdio.h>
20 #include <fcntl.h>
21 #include <ctype.h>
22 #include <pwd.h>
23 #include <errno.h>
24 #include <sys/types.h>
25
26 #if TIME_WITH_SYS_TIME
27 # include <sys/time.h>
28 # include <time.h>
29 #else
30 # if HAVE_SYS_TIME_H
31 #  include <sys/time.h>
32 # else
33 #  include <time.h>
34 # endif
35 #endif
36
37 #include <sys/wait.h>
38 #include <string.h>
39 #include <limits.h>
40 #include <libcitadel.h>
41 #include "citadel.h"
42 #include "server.h"
43 #include "citserver.h"
44 #include "support.h"
45 #include "config.h"
46 #include "database.h"
47 #include "msgbase.h"
48 #include "internet_addressing.h"
49 #include "ctdl_module.h"
50 #include "serv_sieve.h"
51
52 struct RoomProcList *sieve_list = NULL;
53 char *msiv_extensions = NULL;
54
55
56 /*
57  * Callback function to send libSieve trace messages to Citadel log facility
58  */
59 int ctdl_debug(sieve2_context_t *s, void *my)
60 {
61         syslog(LOG_DEBUG, "%s", sieve2_getvalue_string(s, "message"));
62         return SIEVE2_OK;
63 }
64
65
66 /*
67  * Callback function to log script parsing errors
68  */
69 int ctdl_errparse(sieve2_context_t *s, void *my)
70 {
71         syslog(LOG_WARNING, "Error in script, line %d: %s",
72                   sieve2_getvalue_int(s, "lineno"),
73                   sieve2_getvalue_string(s, "message")
74         );
75         return SIEVE2_OK;
76 }
77
78
79 /*
80  * Callback function to log script execution errors
81  */
82 int ctdl_errexec(sieve2_context_t *s, void *my)
83 {
84         syslog(LOG_WARNING, "Error executing script: %s",
85                   sieve2_getvalue_string(s, "message")
86                 );
87         return SIEVE2_OK;
88 }
89
90
91 /*
92  * Callback function to redirect a message to a different folder
93  */
94 int ctdl_redirect(sieve2_context_t *s, void *my)
95 {
96         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
97         struct CtdlMessage *msg = NULL;
98         recptypes *valid = NULL;
99         char recp[256];
100
101         safestrncpy(recp, sieve2_getvalue_string(s, "address"), sizeof recp);
102
103         syslog(LOG_DEBUG, "Action is REDIRECT, recipient <%s>", recp);
104
105         valid = validate_recipients(recp, NULL, 0);
106         if (valid == NULL) {
107                 syslog(LOG_WARNING, "REDIRECT failed: bad recipient <%s>", recp);
108                 return SIEVE2_ERROR_BADARGS;
109         }
110         if (valid->num_error > 0) {
111                 syslog(LOG_WARNING, "REDIRECT failed: bad recipient <%s>", recp);
112                 free_recipients(valid);
113                 return SIEVE2_ERROR_BADARGS;
114         }
115
116         msg = CtdlFetchMessage(cs->msgnum, 1, 1);
117         if (msg == NULL) {
118                 syslog(LOG_WARNING, "REDIRECT failed: unable to fetch msg %ld", cs->msgnum);
119                 free_recipients(valid);
120                 return SIEVE2_ERROR_BADARGS;
121         }
122
123         CtdlSubmitMsg(msg, valid, NULL, 0);
124         cs->cancel_implicit_keep = 1;
125         free_recipients(valid);
126         CM_Free(msg);
127         return SIEVE2_OK;
128 }
129
130
131 /*
132  * Callback function to indicate that a message *will* be kept in the inbox
133  */
134 int ctdl_keep(sieve2_context_t *s, void *my)
135 {
136         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
137         
138         syslog(LOG_DEBUG, "Action is KEEP");
139
140         cs->keep = 1;
141         cs->cancel_implicit_keep = 1;
142         return SIEVE2_OK;
143 }
144
145
146 /*
147  * Callback function to file a message into a different mailbox
148  */
149 int ctdl_fileinto(sieve2_context_t *s, void *my)
150 {
151         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
152         const char *dest_folder = sieve2_getvalue_string(s, "mailbox");
153         int c;
154         char foldername[256];
155         char original_room_name[ROOMNAMELEN];
156
157         syslog(LOG_DEBUG, "Action is FILEINTO, destination is <%s>", dest_folder);
158
159         /* FILEINTO 'INBOX' is the same thing as KEEP */
160         if ( (!strcasecmp(dest_folder, "INBOX")) || (!strcasecmp(dest_folder, MAILROOM)) ) {
161                 cs->keep = 1;
162                 cs->cancel_implicit_keep = 1;
163                 return SIEVE2_OK;
164         }
165
166         /* Remember what room we came from */
167         safestrncpy(original_room_name, CC->room.QRname, sizeof original_room_name);
168
169         /* First try a mailbox name match (check personal mail folders first) */
170         snprintf(foldername, sizeof foldername, "%010ld.%s", cs->usernum, dest_folder);
171         c = CtdlGetRoom(&CC->room, foldername);
172
173         /* Then a regular room name match (public and private rooms) */
174         if (c != 0) {
175                 safestrncpy(foldername, dest_folder, sizeof foldername);
176                 c = CtdlGetRoom(&CC->room, foldername);
177         }
178
179         if (c != 0) {
180                 syslog(LOG_WARNING, "FILEINTO failed: target <%s> does not exist", dest_folder);
181                 return SIEVE2_ERROR_BADARGS;
182         }
183
184         /* Yes, we actually have to go there */
185         CtdlUserGoto(NULL, 0, 0, NULL, NULL, NULL, NULL);
186
187         c = CtdlSaveMsgPointersInRoom(NULL, &cs->msgnum, 1, 0, NULL, 0);
188
189         /* Go back to the room we came from */
190         if (strcasecmp(original_room_name, CC->room.QRname)) {
191                 CtdlUserGoto(original_room_name, 0, 0, NULL, NULL, NULL, NULL);
192         }
193
194         if (c == 0) {
195                 cs->cancel_implicit_keep = 1;
196                 return SIEVE2_OK;
197         }
198         else {
199                 return SIEVE2_ERROR_BADARGS;
200         }
201 }
202
203
204 /*
205  * Callback function to indicate that a message should be discarded.
206  */
207 int ctdl_discard(sieve2_context_t *s, void *my)
208 {
209         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
210
211         syslog(LOG_DEBUG, "Action is DISCARD");
212
213         /* Cancel the implicit keep.  That's all there is to it. */
214         cs->cancel_implicit_keep = 1;
215         return SIEVE2_OK;
216 }
217
218
219 /*
220  * Callback function to indicate that a message should be rejected
221  */
222 int ctdl_reject(sieve2_context_t *s, void *my)
223 {
224         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
225         char *reject_text = NULL;
226
227         syslog(LOG_DEBUG, "Action is REJECT");
228
229         /* If we don't know who sent the message, do a DISCARD instead. */
230         if (IsEmptyStr(cs->sender)) {
231                 syslog(LOG_INFO, "Unknown sender.  Doing DISCARD instead of REJECT.");
232                 return ctdl_discard(s, my);
233         }
234
235         /* Assemble the reject message. */
236         reject_text = malloc(strlen(sieve2_getvalue_string(s, "message")) + 1024);
237         if (reject_text == NULL) {
238                 return SIEVE2_ERROR_FAIL;
239         }
240
241         sprintf(reject_text, 
242                 "Content-type: text/plain\n"
243                 "\n"
244                 "The message was refused by the recipient's mail filtering program.\n"
245                 "The reason given was as follows:\n"
246                 "\n"
247                 "%s\n"
248                 "\n"
249         ,
250                 sieve2_getvalue_string(s, "message")
251         );
252
253         quickie_message(        /* This delivers the message */
254                 NULL,
255                 cs->envelope_to,
256                 cs->sender,
257                 NULL,
258                 reject_text,
259                 FMT_RFC822,
260                 "Delivery status notification"
261         );
262
263         free(reject_text);
264         cs->cancel_implicit_keep = 1;
265         return SIEVE2_OK;
266 }
267
268
269 /*
270  * Callback function to indicate that a vacation message should be generated
271  */
272 int ctdl_vacation(sieve2_context_t *s, void *my)
273 {
274         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
275         struct sdm_vacation *vptr;
276         int days = 1;
277         const char *message;
278         char *vacamsg_text = NULL;
279         char vacamsg_subject[1024];
280
281         syslog(LOG_DEBUG, "Action is VACATION");
282
283         message = sieve2_getvalue_string(s, "message");
284         if (message == NULL) return SIEVE2_ERROR_BADARGS;
285
286         if (sieve2_getvalue_string(s, "subject") != NULL) {
287                 safestrncpy(vacamsg_subject, sieve2_getvalue_string(s, "subject"), sizeof vacamsg_subject);
288         }
289         else {
290                 snprintf(vacamsg_subject, sizeof vacamsg_subject, "Re: %s", cs->subject);
291         }
292
293         days = sieve2_getvalue_int(s, "days");
294         if (days < 1) days = 1;
295         if (days > MAX_VACATION) days = MAX_VACATION;
296
297         /* Check to see whether we've already alerted this sender that we're on vacation. */
298         for (vptr = cs->u->first_vacation; vptr != NULL; vptr = vptr->next) {
299                 if (!strcasecmp(vptr->fromaddr, cs->sender)) {
300                         if ( (time(NULL) - vptr->timestamp) < (days * 86400) ) {
301                                 syslog(LOG_DEBUG, "Already alerted <%s> recently.", cs->sender);
302                                 return SIEVE2_OK;
303                         }
304                 }
305         }
306
307         /* Assemble the reject message. */
308         vacamsg_text = malloc(strlen(message) + 1024);
309         if (vacamsg_text == NULL) {
310                 return SIEVE2_ERROR_FAIL;
311         }
312
313         sprintf(vacamsg_text, 
314                 "Content-type: text/plain charset=utf-8\n"
315                 "\n"
316                 "%s\n"
317                 "\n"
318         ,
319                 message
320         );
321
322         quickie_message(        /* This delivers the message */
323                 NULL,
324                 cs->envelope_to,
325                 cs->sender,
326                 NULL,
327                 vacamsg_text,
328                 FMT_RFC822,
329                 vacamsg_subject
330         );
331
332         free(vacamsg_text);
333
334         /* Now update the list to reflect the fact that we've alerted this sender.
335          * If they're already in the list, just update the timestamp.
336          */
337         for (vptr = cs->u->first_vacation; vptr != NULL; vptr = vptr->next) {
338                 if (!strcasecmp(vptr->fromaddr, cs->sender)) {
339                         vptr->timestamp = time(NULL);
340                         return SIEVE2_OK;
341                 }
342         }
343
344         /* If we get to this point, create a new record.
345          */
346         vptr = malloc(sizeof(struct sdm_vacation));
347         memset(vptr, 0, sizeof(struct sdm_vacation));
348         vptr->timestamp = time(NULL);
349         safestrncpy(vptr->fromaddr, cs->sender, sizeof vptr->fromaddr);
350         vptr->next = cs->u->first_vacation;
351         cs->u->first_vacation = vptr;
352
353         return SIEVE2_OK;
354 }
355
356
357 #if 0
358 /*
359  * Callback function to parse addresses per local system convention
360  * It is disabled because we don't support subaddresses.
361  */
362 int ctdl_getsubaddress(sieve2_context_t *s, void *my)
363 {
364         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
365
366         /* libSieve does not take ownership of the memory used here.  But, since we
367          * are just pointing to locations inside a struct which we are going to free
368          * later, we're ok.
369          */
370         sieve2_setvalue_string(s, "user", cs->recp_user);
371         sieve2_setvalue_string(s, "detail", "");
372         sieve2_setvalue_string(s, "localpart", cs->recp_user);
373         sieve2_setvalue_string(s, "domain", cs->recp_node);
374         return SIEVE2_OK;
375 }
376 #endif
377
378
379 /*
380  * Callback function to parse message envelope
381  */
382 int ctdl_getenvelope(sieve2_context_t *s, void *my)
383 {
384         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
385
386         syslog(LOG_DEBUG, "Action is GETENVELOPE");
387         syslog(LOG_DEBUG, "EnvFrom: %s", cs->envelope_from);
388         syslog(LOG_DEBUG, "EnvTo: %s", cs->envelope_to);
389
390         if (cs->envelope_from != NULL) {
391                 if ((cs->envelope_from[0] != '@')&&(cs->envelope_from[strlen(cs->envelope_from)-1] != '@')) {
392                         sieve2_setvalue_string(s, "from", cs->envelope_from);
393                 }
394                 else {
395                         sieve2_setvalue_string(s, "from", "invalid_envelope_from@example.org");
396                 }
397         }
398         else {
399                 sieve2_setvalue_string(s, "from", "null_envelope_from@example.org");
400         }
401
402
403         if (cs->envelope_to != NULL) {
404                 if ((cs->envelope_to[0] != '@') && (cs->envelope_to[strlen(cs->envelope_to)-1] != '@')) {
405                         sieve2_setvalue_string(s, "to", cs->envelope_to);
406                 }
407                 else {
408                         sieve2_setvalue_string(s, "to", "invalid_envelope_to@example.org");
409                 }
410         }
411         else {
412                 sieve2_setvalue_string(s, "to", "null_envelope_to@example.org");
413         }
414
415         return SIEVE2_OK;
416 }
417
418
419 #if 0
420 /*
421  * Callback function to fetch message body
422  * (Uncomment the code if we implement this extension)
423  *
424  */
425 int ctdl_getbody(sieve2_context_t *s, void *my)
426 {
427         return SIEVE2_ERROR_UNSUPPORTED;
428 }
429 #endif
430
431
432 /*
433  * Callback function to fetch message size
434  */
435 int ctdl_getsize(sieve2_context_t *s, void *my)
436 {
437         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
438         struct MetaData smi;
439
440         GetMetaData(&smi, cs->msgnum);
441         
442         if (smi.meta_rfc822_length > 0L) {
443                 sieve2_setvalue_int(s, "size", (int)smi.meta_rfc822_length);
444                 return SIEVE2_OK;
445         }
446
447         return SIEVE2_ERROR_UNSUPPORTED;
448 }
449
450
451 /*
452  * Return a pointer to the active Sieve script.
453  * (Caller does NOT own the memory and should not free the returned pointer.)
454  */
455 char *get_active_script(struct sdm_userdata *u) {
456         struct sdm_script *sptr;
457
458         for (sptr=u->first_script; sptr!=NULL; sptr=sptr->next) {
459                 if (sptr->script_active > 0) {
460                         syslog(LOG_DEBUG, "get_active_script() is using script '%s'", sptr->script_name);
461                         return(sptr->script_content);
462                 }
463         }
464
465         syslog(LOG_DEBUG, "get_active_script() found no active script");
466         return(NULL);
467 }
468
469
470 /*
471  * Callback function to retrieve the sieve script
472  */
473 int ctdl_getscript(sieve2_context_t *s, void *my) {
474         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
475
476         char *active_script = get_active_script(cs->u);
477         if (active_script != NULL) {
478                 sieve2_setvalue_string(s, "script", active_script);
479                 return SIEVE2_OK;
480         }
481
482         return SIEVE2_ERROR_GETSCRIPT;
483 }
484
485
486 /*
487  * Callback function to retrieve message headers
488  */
489 int ctdl_getheaders(sieve2_context_t *s, void *my) {
490
491         struct ctdl_sieve *cs = (struct ctdl_sieve *)my;
492
493         syslog(LOG_DEBUG, "ctdl_getheaders() was called");
494         sieve2_setvalue_string(s, "allheaders", cs->rfc822headers);
495         return SIEVE2_OK;
496 }
497
498
499 /*
500  * Add a room to the list of those rooms which potentially require sieve processing
501  */
502 void sieve_queue_room(struct ctdlroom *which_room) {
503         struct RoomProcList *ptr;
504
505         ptr = (struct RoomProcList *) malloc(sizeof (struct RoomProcList));
506         if (ptr == NULL) return;
507
508         safestrncpy(ptr->name, which_room->QRname, sizeof ptr->name);
509         begin_critical_section(S_SIEVELIST);
510         ptr->next = sieve_list;
511         sieve_list = ptr;
512         end_critical_section(S_SIEVELIST);
513         syslog(LOG_DEBUG, "<%s> queued for Sieve processing", which_room->QRname);
514 }
515
516
517 /*
518  * Perform sieve processing for one message (called by sieve_do_room() for each message)
519  */
520 void sieve_do_msg(long msgnum, void *userdata) {
521         struct sdm_userdata *u = (struct sdm_userdata *) userdata;
522         sieve2_context_t *sieve2_context;
523         struct ctdl_sieve my;
524         int res;
525         struct CtdlMessage *msg;
526         int i;
527         size_t headers_len = 0;
528         int len = 0;
529
530         if (u == NULL) {
531                 syslog(LOG_ERR, "Can't process message <%ld> without userdata!", msgnum);
532                 return;
533         }
534
535         sieve2_context = u->sieve2_context;
536
537         syslog(LOG_DEBUG, "Performing sieve processing on msg <%ld>", msgnum);
538
539         /*
540          * Make sure you include message body so you can get those second-level headers ;)
541          */
542         msg = CtdlFetchMessage(msgnum, 1, 1);
543         if (msg == NULL) return;
544
545         /*
546          * Grab the message headers so we can feed them to libSieve.
547          * Use HEADERS_ONLY rather than HEADERS_FAST in order to include second-level headers.
548          */
549         CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
550         CtdlOutputPreLoadedMsg(msg, MT_RFC822, HEADERS_ONLY, 0, 1, 0);
551         headers_len = StrLength(CC->redirect_buffer);
552         my.rfc822headers = SmashStrBuf(&CC->redirect_buffer);
553
554         /*
555          * libSieve clobbers the stack if it encounters badly formed
556          * headers.  Sanitize our headers by stripping nonprintable
557          * characters.
558          */
559         for (i=0; i<headers_len; ++i) {
560                 if (!isascii(my.rfc822headers[i])) {
561                         my.rfc822headers[i] = '_';
562                 }
563         }
564
565         my.keep = 0;                            /* Set to 1 to declare an *explicit* keep */
566         my.cancel_implicit_keep = 0;            /* Some actions will cancel the implicit keep */
567         my.usernum = atol(CC->room.QRname);     /* Keep track of the owner of the room's namespace */
568         my.msgnum = msgnum;                     /* Keep track of the message number in our local store */
569         my.u = u;                               /* Hand off a pointer to the rest of this info */
570
571         /* Keep track of the recipient so we can do handling based on it later */
572         process_rfc822_addr(msg->cm_fields[eRecipient], my.recp_user, my.recp_node, my.recp_name);
573
574         /* Keep track of the sender so we can use it for REJECT and VACATION responses */
575         if (!CM_IsEmpty(msg, erFc822Addr)) {
576                 safestrncpy(my.sender, msg->cm_fields[erFc822Addr], sizeof my.sender);
577         }
578         else if (!CM_IsEmpty(msg, eAuthor)) {
579                 safestrncpy(my.sender, msg->cm_fields[eAuthor], sizeof my.sender);
580         }
581         else {
582                 strcpy(my.sender, "");
583         }
584
585         /* Keep track of the subject so we can use it for VACATION responses */
586         if (!CM_IsEmpty(msg, eMsgSubject)) {
587                 safestrncpy(my.subject, msg->cm_fields[eMsgSubject], sizeof my.subject);
588         }
589         else {
590                 strcpy(my.subject, "");
591         }
592
593         /* Keep track of the envelope-from address (use body-from if not found) */
594         if (!CM_IsEmpty(msg, eMessagePath)) {
595                 safestrncpy(my.envelope_from, msg->cm_fields[eMessagePath], sizeof my.envelope_from);
596                 stripallbut(my.envelope_from, '<', '>');
597         }
598         else if (!CM_IsEmpty(msg, erFc822Addr)) {
599                 safestrncpy(my.envelope_from, msg->cm_fields[erFc822Addr], sizeof my.envelope_from);
600                 stripallbut(my.envelope_from, '<', '>');
601         }
602         else {
603                 strcpy(my.envelope_from, "");
604         }
605
606         len = strlen(my.envelope_from);
607         for (i=0; i<len; ++i) {
608                 if (isspace(my.envelope_from[i])) my.envelope_from[i] = '_';
609         }
610         if (haschar(my.envelope_from, '@') == 0) {
611                 strcat(my.envelope_from, "@");
612                 strcat(my.envelope_from, CtdlGetConfigStr("c_fqdn"));
613         }
614
615         /* Keep track of the envelope-to address (use body-to if not found) */
616         if (!CM_IsEmpty(msg, eenVelopeTo)) {
617                 safestrncpy(my.envelope_to, msg->cm_fields[eenVelopeTo], sizeof my.envelope_to);
618                 stripallbut(my.envelope_to, '<', '>');
619         }
620         else if (!CM_IsEmpty(msg, eRecipient)) {
621                 safestrncpy(my.envelope_to, msg->cm_fields[eRecipient], sizeof my.envelope_to);
622                 if (!CM_IsEmpty(msg, eDestination)) {
623                         strcat(my.envelope_to, "@");
624                         strcat(my.envelope_to, msg->cm_fields[eDestination]);
625                 }
626                 stripallbut(my.envelope_to, '<', '>');
627         }
628         else {
629                 strcpy(my.envelope_to, "");
630         }
631
632         len = strlen(my.envelope_to);
633         for (i=0; i<len; ++i) {
634                 if (isspace(my.envelope_to[i])) my.envelope_to[i] = '_';
635         }
636         if (haschar(my.envelope_to, '@') == 0) {
637                 strcat(my.envelope_to, "@");
638                 strcat(my.envelope_to, CtdlGetConfigStr("c_fqdn"));
639         }
640
641         CM_Free(msg);
642         
643         syslog(LOG_DEBUG, "Calling sieve2_execute()");
644         res = sieve2_execute(sieve2_context, &my);
645         if (res != SIEVE2_OK) {
646                 syslog(LOG_ERR, "sieve2_execute() returned %d: %s", res, sieve2_errstr(res));
647         }
648
649         free(my.rfc822headers);
650         my.rfc822headers = NULL;
651
652         /*
653          * Delete the message from the inbox unless either we were told not to, or
654          * if no other action was successfully taken.
655          */
656         if ( (!my.keep) && (my.cancel_implicit_keep) ) {
657                 syslog(LOG_DEBUG, "keep is 0 -- deleting message from inbox");
658                 CtdlDeleteMessages(CC->room.QRname, &msgnum, 1, "");
659         }
660
661         syslog(LOG_DEBUG, "Completed sieve processing on msg <%ld>", msgnum);
662         u->lastproc = msgnum;
663
664         return;
665 }
666
667
668
669 /*
670  * Given the on-disk representation of our Sieve config, load
671  * it into an in-memory data structure.
672  */
673 void parse_sieve_config(char *conf, struct sdm_userdata *u) {
674         char *ptr;
675         char *c, *vacrec;
676         char keyword[256];
677         struct sdm_script *sptr;
678         struct sdm_vacation *vptr;
679
680         ptr = conf;
681         while (c = ptr, ptr = bmstrcasestr(ptr, CTDLSIEVECONFIGSEPARATOR), ptr != NULL) {
682                 *ptr = 0;
683                 ptr += strlen(CTDLSIEVECONFIGSEPARATOR);
684
685                 extract_token(keyword, c, 0, '|', sizeof keyword);
686
687                 if (!strcasecmp(keyword, "lastproc")) {
688                         u->lastproc = extract_long(c, 1);
689                 }
690
691                 else if (!strcasecmp(keyword, "script")) {
692                         sptr = malloc(sizeof(struct sdm_script));
693                         extract_token(sptr->script_name, c, 1, '|', sizeof sptr->script_name);
694                         sptr->script_active = extract_int(c, 2);
695                         remove_token(c, 0, '|');
696                         remove_token(c, 0, '|');
697                         remove_token(c, 0, '|');
698                         sptr->script_content = strdup(c);
699                         sptr->next = u->first_script;
700                         u->first_script = sptr;
701                 }
702
703                 else if (!strcasecmp(keyword, "vacation")) {
704
705                         if (c != NULL) while (vacrec=c, c=strchr(c, '\n'), (c != NULL)) {
706
707                                 *c = 0;
708                                 ++c;
709
710                                 if (strncasecmp(vacrec, "vacation|", 9)) {
711                                         vptr = malloc(sizeof(struct sdm_vacation));
712                                         extract_token(vptr->fromaddr, vacrec, 0, '|', sizeof vptr->fromaddr);
713                                         vptr->timestamp = extract_long(vacrec, 1);
714                                         vptr->next = u->first_vacation;
715                                         u->first_vacation = vptr;
716                                 }
717                         }
718                 }
719
720                 /* ignore unknown keywords */
721         }
722 }
723
724 /*
725  * We found the Sieve configuration for this user.
726  * Now do something with it.
727  */
728 void get_sieve_config_backend(long msgnum, void *userdata) {
729         struct sdm_userdata *u = (struct sdm_userdata *) userdata;
730         struct CtdlMessage *msg;
731         char *conf;
732         long conflen;
733
734         u->config_msgnum = msgnum;
735         msg = CtdlFetchMessage(msgnum, 1, 1);
736         if (msg == NULL) {
737                 u->config_msgnum = (-1) ;
738                 return;
739         }
740
741         CM_GetAsField(msg, eMesageText, &conf, &conflen);
742
743         CM_Free(msg);
744
745         if (conf != NULL) {
746                 parse_sieve_config(conf, u);
747                 free(conf);
748         }
749
750 }
751
752
753 /* 
754  * Write our citadel sieve config back to disk
755  * 
756  * (Set yes_write_to_disk to nonzero to make it actually write the config;
757  * otherwise it just frees the data structures.)
758  */
759 void rewrite_ctdl_sieve_config(struct sdm_userdata *u, int yes_write_to_disk) {
760         StrBuf *text;
761         struct sdm_script *sptr;
762         struct sdm_vacation *vptr;
763         
764         text = NewStrBufPlain(NULL, SIZ);
765         StrBufPrintf(text,
766                      "Content-type: application/x-citadel-sieve-config\n"
767                      "\n"
768                      CTDLSIEVECONFIGSEPARATOR
769                      "lastproc|%ld"
770                      CTDLSIEVECONFIGSEPARATOR
771                      ,
772                      u->lastproc
773                 );
774
775         while (u->first_script != NULL) {
776                 StrBufAppendPrintf(text,
777                                    "script|%s|%d|%s" CTDLSIEVECONFIGSEPARATOR,
778                                    u->first_script->script_name,
779                                    u->first_script->script_active,
780                                    u->first_script->script_content
781                         );
782                 sptr = u->first_script;
783                 u->first_script = u->first_script->next;
784                 free(sptr->script_content);
785                 free(sptr);
786         }
787
788         if (u->first_vacation != NULL) {
789
790                 StrBufAppendPrintf(text, "vacation|\n");
791                 while (u->first_vacation != NULL) {
792                         if ( (time(NULL) - u->first_vacation->timestamp) < (MAX_VACATION * 86400)) {
793                                 StrBufAppendPrintf(text, "%s|%ld\n",
794                                                    u->first_vacation->fromaddr,
795                                                    u->first_vacation->timestamp
796                                         );
797                         }
798                         vptr = u->first_vacation;
799                         u->first_vacation = u->first_vacation->next;
800                         free(vptr);
801                 }
802                 StrBufAppendPrintf(text, CTDLSIEVECONFIGSEPARATOR);
803         }
804
805         if (yes_write_to_disk)
806         {
807                 /* Save the config */
808                 quickie_message("Citadel", NULL, NULL, u->config_roomname,
809                                 ChrPtr(text),
810                                 4,
811                                 "Sieve configuration"
812                 );
813                 
814                 /* And delete the old one */
815                 if (u->config_msgnum > 0) {
816                         CtdlDeleteMessages(u->config_roomname, &u->config_msgnum, 1, "");
817                 }
818         }
819
820         FreeStrBuf (&text);
821
822 }
823
824
825 /*
826  * This is our callback registration table for libSieve.
827  */
828 sieve2_callback_t ctdl_sieve_callbacks[] = {
829         { SIEVE2_ACTION_REJECT,         ctdl_reject             },
830         { SIEVE2_ACTION_VACATION,       ctdl_vacation           },
831         { SIEVE2_ERRCALL_PARSE,         ctdl_errparse           },
832         { SIEVE2_ERRCALL_RUNTIME,       ctdl_errexec            },
833         { SIEVE2_ACTION_FILEINTO,       ctdl_fileinto           },
834         { SIEVE2_ACTION_REDIRECT,       ctdl_redirect           },
835         { SIEVE2_ACTION_DISCARD,        ctdl_discard            },
836         { SIEVE2_ACTION_KEEP,           ctdl_keep               },
837         { SIEVE2_SCRIPT_GETSCRIPT,      ctdl_getscript          },
838         { SIEVE2_DEBUG_TRACE,           ctdl_debug              },
839         { SIEVE2_MESSAGE_GETALLHEADERS, ctdl_getheaders         },
840         { SIEVE2_MESSAGE_GETSIZE,       ctdl_getsize            },
841         { SIEVE2_MESSAGE_GETENVELOPE,   ctdl_getenvelope        },
842 /*
843  * These actions are unsupported by Citadel so we don't declare them.
844  *
845         { SIEVE2_ACTION_NOTIFY,         ctdl_notify             },
846         { SIEVE2_MESSAGE_GETSUBADDRESS, ctdl_getsubaddress      },
847         { SIEVE2_MESSAGE_GETBODY,       ctdl_getbody            },
848  *
849  */
850         { 0 }
851 };
852
853
854 /*
855  * Perform sieve processing for a single room
856  */
857 void sieve_do_room(char *roomname) {
858         
859         struct sdm_userdata u;
860         sieve2_context_t *sieve2_context = NULL;        /* Context for sieve parser */
861         int res;                                        /* Return code from libsieve calls */
862         long orig_lastproc = 0;
863
864         memset(&u, 0, sizeof u);
865
866         /* See if the user who owns this 'mailbox' has any Sieve scripts that
867          * require execution.
868          */
869         snprintf(u.config_roomname, sizeof u.config_roomname, "%010ld.%s", atol(roomname), USERCONFIGROOM);
870         if (CtdlGetRoom(&CC->room, u.config_roomname) != 0) {
871                 syslog(LOG_DEBUG, "<%s> does not exist.  No processing is required.", u.config_roomname);
872                 return;
873         }
874
875         /*
876          * Find the sieve scripts and control record and do something
877          */
878         u.config_msgnum = (-1);
879         CtdlForEachMessage(MSGS_LAST, 1, NULL, SIEVECONFIG, NULL,
880                 get_sieve_config_backend, (void *)&u );
881
882         if (u.config_msgnum < 0) {
883                 syslog(LOG_DEBUG, "No Sieve rules exist.  No processing is required.");
884                 return;
885         }
886
887         /*
888          * Check to see whether the script is empty and should not be processed.
889          * A script is considered non-empty if it contains at least one semicolon.
890          */
891         if (
892                 (get_active_script(&u) == NULL)
893                 || (strchr(get_active_script(&u), ';') == NULL)
894         ) {
895                 syslog(LOG_DEBUG, "Sieve script is empty.  No processing is required.");
896                 return;
897         }
898
899         syslog(LOG_DEBUG, "Rules found.  Performing Sieve processing for <%s>", roomname);
900
901         if (CtdlGetRoom(&CC->room, roomname) != 0) {
902                 syslog(LOG_ERR, "ERROR: cannot load <%s>", roomname);
903                 return;
904         }
905
906         /* Initialize the Sieve parser */
907         
908         res = sieve2_alloc(&sieve2_context);
909         if (res != SIEVE2_OK) {
910                 syslog(LOG_ERR, "sieve2_alloc() returned %d: %s", res, sieve2_errstr(res));
911                 return;
912         }
913
914         res = sieve2_callbacks(sieve2_context, ctdl_sieve_callbacks);
915         if (res != SIEVE2_OK) {
916                 syslog(LOG_ERR, "sieve2_callbacks() returned %d: %s", res, sieve2_errstr(res));
917                 goto BAIL;
918         }
919
920         /* Validate the script */
921
922         struct ctdl_sieve my;           /* dummy ctdl_sieve struct just to pass "u" slong */
923         memset(&my, 0, sizeof my);
924         my.u = &u;
925         res = sieve2_validate(sieve2_context, &my);
926         if (res != SIEVE2_OK) {
927                 syslog(LOG_ERR, "sieve2_validate() returned %d: %s", res, sieve2_errstr(res));
928                 goto BAIL;
929         }
930
931         /* Do something useful */
932         u.sieve2_context = sieve2_context;
933         orig_lastproc = u.lastproc;
934         CtdlForEachMessage(MSGS_GT, u.lastproc, NULL, NULL, NULL,
935                 sieve_do_msg,
936                 (void *) &u
937         );
938
939 BAIL:
940         res = sieve2_free(&sieve2_context);
941         if (res != SIEVE2_OK) {
942                 syslog(LOG_ERR, "sieve2_free() returned %d: %s", res, sieve2_errstr(res));
943         }
944
945         /* Rewrite the config if we have to */
946         rewrite_ctdl_sieve_config(&u, (u.lastproc > orig_lastproc) ) ;
947 }
948
949
950 /*
951  * Perform sieve processing for all rooms which require it
952  */
953 void perform_sieve_processing(void) {
954         struct RoomProcList *ptr = NULL;
955
956         if (sieve_list != NULL) {
957                 syslog(LOG_DEBUG, "Begin Sieve processing");
958                 while (sieve_list != NULL) {
959                         char spoolroomname[ROOMNAMELEN];
960                         safestrncpy(spoolroomname, sieve_list->name, sizeof spoolroomname);
961                         begin_critical_section(S_SIEVELIST);
962
963                         /* pop this record off the list */
964                         ptr = sieve_list;
965                         sieve_list = sieve_list->next;
966                         free(ptr);
967
968                         /* invalidate any duplicate entries to prevent double processing */
969                         for (ptr=sieve_list; ptr!=NULL; ptr=ptr->next) {
970                                 if (!strcasecmp(ptr->name, spoolroomname)) {
971                                         ptr->name[0] = 0;
972                                 }
973                         }
974
975                         end_critical_section(S_SIEVELIST);
976                         if (spoolroomname[0] != 0) {
977                                 sieve_do_room(spoolroomname);
978                         }
979                 }
980         }
981 }
982
983
984 void msiv_load(struct sdm_userdata *u) {
985         char hold_rm[ROOMNAMELEN];
986
987         strcpy(hold_rm, CC->room.QRname);       /* save current room */
988
989         /* Take a spin through the user's personal address book */
990         if (CtdlGetRoom(&CC->room, USERCONFIGROOM) == 0) {
991         
992                 u->config_msgnum = (-1);
993                 strcpy(u->config_roomname, CC->room.QRname);
994                 CtdlForEachMessage(MSGS_LAST, 1, NULL, SIEVECONFIG, NULL,
995                         get_sieve_config_backend, (void *)u );
996
997         }
998
999         if (strcmp(CC->room.QRname, hold_rm)) {
1000                 CtdlGetRoom(&CC->room, hold_rm);    /* return to saved room */
1001         }
1002 }
1003
1004 void msiv_store(struct sdm_userdata *u, int yes_write_to_disk) {
1005 /*
1006  * Initialise the sieve configs last processed message number.
1007  * We don't need to get the highest message number for the users inbox since the systems
1008  * highest message number will be higher than that and loer than this scripts message number
1009  * This prevents this new script from processing any old messages in the inbox.
1010  * Most importantly it will prevent vacation messages being sent to lots of old messages
1011  * in the inbox.
1012  */
1013         u->lastproc = CtdlGetCurrentMessageNumber();
1014         rewrite_ctdl_sieve_config(u, yes_write_to_disk);
1015 }
1016
1017
1018 /*
1019  * Select the active script.
1020  * (Set script_name to an empty string to disable all scripts)
1021  * 
1022  * Returns 0 on success or nonzero for error.
1023  */
1024 int msiv_setactive(struct sdm_userdata *u, char *script_name) {
1025         int ok = 0;
1026         struct sdm_script *s;
1027
1028         /* First see if the supplied value is ok */
1029
1030         if (IsEmptyStr(script_name)) {
1031                 ok = 1;
1032         }
1033         else {
1034                 for (s=u->first_script; s!=NULL; s=s->next) {
1035                         if (!strcasecmp(s->script_name, script_name)) {
1036                                 ok = 1;
1037                         }
1038                 }
1039         }
1040
1041         if (!ok) return(-1);
1042
1043         /* Now set the active script */
1044         for (s=u->first_script; s!=NULL; s=s->next) {
1045                 if (!strcasecmp(s->script_name, script_name)) {
1046                         s->script_active = 1;
1047                 }
1048                 else {
1049                         s->script_active = 0;
1050                 }
1051         }
1052         
1053         return(0);
1054 }
1055
1056
1057 /*
1058  * Fetch a script by name.
1059  *
1060  * Returns NULL if the named script was not found, or a pointer to the script
1061  * if it was found.   NOTE: the caller does *not* own the memory returned by
1062  * this function.  Copy it if you need to keep it.
1063  */
1064 char *msiv_getscript(struct sdm_userdata *u, char *script_name) {
1065         struct sdm_script *s;
1066
1067         for (s=u->first_script; s!=NULL; s=s->next) {
1068                 if (!strcasecmp(s->script_name, script_name)) {
1069                         if (s->script_content != NULL) {
1070                                 return (s->script_content);
1071                         }
1072                 }
1073         }
1074
1075         return(NULL);
1076 }
1077
1078
1079 /*
1080  * Delete a script by name.
1081  *
1082  * Returns 0 if the script was deleted.
1083  *       1 if the script was not found.
1084  *       2 if the script cannot be deleted because it is active.
1085  */
1086 int msiv_deletescript(struct sdm_userdata *u, char *script_name) {
1087         struct sdm_script *s = NULL;
1088         struct sdm_script *script_to_delete = NULL;
1089
1090         for (s=u->first_script; s!=NULL; s=s->next) {
1091                 if (!strcasecmp(s->script_name, script_name)) {
1092                         script_to_delete = s;
1093                         if (s->script_active) {
1094                                 return(2);
1095                         }
1096                 }
1097         }
1098
1099         if (script_to_delete == NULL) return(1);
1100
1101         if (u->first_script == script_to_delete) {
1102                 u->first_script = u->first_script->next;
1103         }
1104         else for (s=u->first_script; s!=NULL; s=s->next) {
1105                 if (s->next == script_to_delete) {
1106                         s->next = s->next->next;
1107                 }
1108         }
1109
1110         free(script_to_delete->script_content);
1111         free(script_to_delete);
1112         return(0);
1113 }
1114
1115
1116 /*
1117  * Add or replace a new script.  
1118  * NOTE: after this function returns, "u" owns the memory that "script_content"
1119  * was pointing to.
1120  */
1121 void msiv_putscript(struct sdm_userdata *u, char *script_name, char *script_content) {
1122         int replaced = 0;
1123         struct sdm_script *s, *sptr;
1124
1125         for (s=u->first_script; s!=NULL; s=s->next) {
1126                 if (!strcasecmp(s->script_name, script_name)) {
1127                         if (s->script_content != NULL) {
1128                                 free(s->script_content);
1129                         }
1130                         s->script_content = script_content;
1131                         replaced = 1;
1132                 }
1133         }
1134
1135         if (replaced == 0) {
1136                 sptr = malloc(sizeof(struct sdm_script));
1137                 safestrncpy(sptr->script_name, script_name, sizeof sptr->script_name);
1138                 sptr->script_content = script_content;
1139                 sptr->script_active = 0;
1140                 sptr->next = u->first_script;
1141                 u->first_script = sptr;
1142         }
1143 }
1144
1145
1146
1147 /*
1148  * Citadel protocol to manage sieve scripts.
1149  * This is basically a simplified (read: doesn't resemble IMAP) version
1150  * of the 'managesieve' protocol.
1151  */
1152 void cmd_msiv(char *argbuf) {
1153         char subcmd[256];
1154         struct sdm_userdata u;
1155         char script_name[256];
1156         char *script_content = NULL;
1157         struct sdm_script *s;
1158         int i;
1159         int changes_made = 0;
1160
1161         memset(&u, 0, sizeof(struct sdm_userdata));
1162
1163         if (CtdlAccessCheck(ac_logged_in)) return;
1164         extract_token(subcmd, argbuf, 0, '|', sizeof subcmd);
1165         msiv_load(&u);
1166
1167         if (!strcasecmp(subcmd, "putscript")) {
1168                 extract_token(script_name, argbuf, 1, '|', sizeof script_name);
1169                 if (!IsEmptyStr(script_name)) {
1170                         cprintf("%d Transmit script now\n", SEND_LISTING);
1171                         script_content = CtdlReadMessageBody(HKEY("000"), CtdlGetConfigLong("c_maxmsglen"), NULL, 0);
1172                         msiv_putscript(&u, script_name, script_content);
1173                         changes_made = 1;
1174                 }
1175                 else {
1176                         cprintf("%d Invalid script name.\n", ERROR + ILLEGAL_VALUE);
1177                 }
1178         }       
1179         
1180         else if (!strcasecmp(subcmd, "listscripts")) {
1181                 cprintf("%d Scripts:\n", LISTING_FOLLOWS);
1182                 for (s=u.first_script; s!=NULL; s=s->next) {
1183                         if (s->script_content != NULL) {
1184                                 cprintf("%s|%d|\n", s->script_name, s->script_active);
1185                         }
1186                 }
1187                 cprintf("000\n");
1188         }
1189
1190         else if (!strcasecmp(subcmd, "setactive")) {
1191                 extract_token(script_name, argbuf, 1, '|', sizeof script_name);
1192                 if (msiv_setactive(&u, script_name) == 0) {
1193                         cprintf("%d ok\n", CIT_OK);
1194                         changes_made = 1;
1195                 }
1196                 else {
1197                         cprintf("%d Script '%s' does not exist.\n",
1198                                 ERROR + ILLEGAL_VALUE,
1199                                 script_name
1200                         );
1201                 }
1202         }
1203
1204         else if (!strcasecmp(subcmd, "getscript")) {
1205                 extract_token(script_name, argbuf, 1, '|', sizeof script_name);
1206                 script_content = msiv_getscript(&u, script_name);
1207                 if (script_content != NULL) {
1208                         int script_len;
1209
1210                         cprintf("%d Script:\n", LISTING_FOLLOWS);
1211                         script_len = strlen(script_content);
1212                         client_write(script_content, script_len);
1213                         if (script_content[script_len-1] != '\n') {
1214                                 cprintf("\n");
1215                         }
1216                         cprintf("000\n");
1217                 }
1218                 else {
1219                         cprintf("%d Invalid script name.\n", ERROR + ILLEGAL_VALUE);
1220                 }
1221         }
1222
1223         else if (!strcasecmp(subcmd, "deletescript")) {
1224                 extract_token(script_name, argbuf, 1, '|', sizeof script_name);
1225                 i = msiv_deletescript(&u, script_name);
1226                 if (i == 0) {
1227                         cprintf("%d ok\n", CIT_OK);
1228                         changes_made = 1;
1229                 }
1230                 else if (i == 1) {
1231                         cprintf("%d Script '%s' does not exist.\n",
1232                                 ERROR + ILLEGAL_VALUE,
1233                                 script_name
1234                         );
1235                 }
1236                 else if (i == 2) {
1237                         cprintf("%d Script '%s' is active and cannot be deleted.\n",
1238                                 ERROR + ILLEGAL_VALUE,
1239                                 script_name
1240                         );
1241                 }
1242                 else {
1243                         cprintf("%d unknown error\n", ERROR);
1244                 }
1245         }
1246
1247         else {
1248                 cprintf("%d Invalid subcommand\n", ERROR + CMD_NOT_SUPPORTED);
1249         }
1250
1251         msiv_store(&u, changes_made);
1252 }
1253
1254
1255
1256 void ctdl_sieve_init(void) {
1257         char *cred = NULL;
1258         sieve2_context_t *sieve2_context = NULL;
1259         int res;
1260
1261         /*
1262          *      We don't really care about dumping the entire credits to the log
1263          *      every time the server is initialized.  The documentation will suffice
1264          *      for that purpose.  We are making a call to sieve2_credits() in order
1265          *      to demonstrate that we have successfully linked in to libsieve.
1266          */
1267         cred = strdup(sieve2_credits());
1268         if (cred == NULL) return;
1269
1270         if (strlen(cred) > 60) {
1271                 strcpy(&cred[55], "...");
1272         }
1273
1274         syslog(LOG_INFO, "%s",cred);
1275         free(cred);
1276
1277         /* Briefly initialize a Sieve parser instance just so we can list the
1278          * extensions that are available.
1279          */
1280         res = sieve2_alloc(&sieve2_context);
1281         if (res != SIEVE2_OK) {
1282                 syslog(LOG_ERR, "sieve2_alloc() returned %d: %s", res, sieve2_errstr(res));
1283                 return;
1284         }
1285
1286         res = sieve2_callbacks(sieve2_context, ctdl_sieve_callbacks);
1287         if (res != SIEVE2_OK) {
1288                 syslog(LOG_ERR, "sieve2_callbacks() returned %d: %s", res, sieve2_errstr(res));
1289                 goto BAIL;
1290         }
1291
1292         msiv_extensions = strdup(sieve2_listextensions(sieve2_context));
1293         syslog(LOG_INFO, "Extensions: %s", msiv_extensions);
1294
1295 BAIL:   res = sieve2_free(&sieve2_context);
1296         if (res != SIEVE2_OK) {
1297                 syslog(LOG_ERR, "sieve2_free() returned %d: %s", res, sieve2_errstr(res));
1298         }
1299
1300 }
1301
1302
1303 void cleanup_sieve(void)
1304 {
1305         struct RoomProcList *ptr, *ptr2;
1306
1307         if (msiv_extensions != NULL)
1308                 free(msiv_extensions);
1309         msiv_extensions = NULL;
1310
1311         begin_critical_section(S_SIEVELIST);
1312         ptr=sieve_list;
1313         while (ptr != NULL) {
1314                 ptr2 = ptr->next;
1315                 free(ptr);
1316                 ptr = ptr2;
1317         }
1318         sieve_list = NULL;
1319         end_critical_section(S_SIEVELIST);
1320 }
1321
1322
1323 int serv_sieve_room(struct ctdlroom *room)
1324 {
1325         if (!strcasecmp(&room->QRname[11], MAILROOM)) {
1326                 sieve_queue_room(room);
1327         }
1328         return 0;
1329 }
1330
1331
1332 CTDL_MODULE_INIT(sieve)
1333 {
1334         if (!threading)
1335         {
1336                 ctdl_sieve_init();
1337                 CtdlRegisterProtoHook(cmd_msiv, "MSIV", "Manage Sieve scripts");
1338                 CtdlRegisterRoomHook(serv_sieve_room);
1339                 CtdlRegisterSessionHook(perform_sieve_processing, EVT_HOUSE, PRIO_HOUSE + 10);
1340                 CtdlRegisterCleanupHook(cleanup_sieve);
1341         }
1342         
1343         /* return our module name for the log */
1344         return "sieve";
1345 }