1 #include <stdlib.h>
2 #include <string.h>
3 #include <unistd.h>
4 
5 #include "mailfront.h"
6 
7 #include <bglibs/cdb.h>
8 #include <bglibs/dict.h>
9 #include <bglibs/dict.h>
10 #include <bglibs/iobuf.h>
11 #include <bglibs/str.h>
12 
13 static RESPONSE(erropen,421,"4.3.0 Could not open $MAILRULES file");
14 static RESPONSE(syntax,421,"4.3.0 Syntax error in $MAILRULES");
15 static RESPONSE(erropenref,421,"4.3.0 Error opening file referenced from $MAILRULES");
16 
17 #define NUL 0
18 
19 /* rule tables ************************************************************* */
20 struct pattern
21 {
22   str pattern;
23   dict* file;
24   struct cdb* cdb;
25   int negated;
26 };
27 
28 struct rule
29 {
30   int code;
31   struct pattern sender;
32   struct pattern recipient;
33   str response;
34   str relayclient;
35   str environment;
36   unsigned long databytes;
37   struct rule* next;
38 };
39 
40 static struct rule* sender_rules = 0;
41 static struct rule* recip_rules = 0;
42 static struct rule** current_rules = 0;
append_rule(struct rule * r)43 static void append_rule(struct rule* r)
44 {
45   static struct rule* sender_tail = 0;
46   static struct rule* recip_tail = 0;
47   struct rule** head;
48   struct rule** tail;
49   if (current_rules != 0)
50     tail = ((head = current_rules) == &sender_rules) ?
51       &sender_tail : &recip_tail;
52   else if (r->recipient.pattern.len == 1 && r->recipient.pattern.s[0] == '*') {
53     head = &sender_rules;
54     tail = &sender_tail;
55   }
56   else {
57     head = &recip_rules;
58     tail = &recip_tail;
59   }
60   if (*tail == 0)
61     *head = r;
62   else
63     (*tail)->next = r;
64   *tail = r;
65 }
66 
alloc_rule(void)67 static struct rule* alloc_rule(void)
68 {
69   struct rule* r;
70   if ((r = malloc(sizeof *r)) != 0)
71     memset(r, 0, sizeof *r);
72   return r;
73 }
74 
75 #if 0
76 static void free_rule(struct rule* r)
77 {
78   str_free(&r->sender.pattern);
79   str_free(&r->recipient.pattern);
80   str_free(&r->response);
81   str_free(&r->relayclient);
82   str_free(&r->environment);
83   free(r);
84 }
85 #endif
86 
87 /* embeded filename handling *********************************************** */
88 static dict cdb_files;
open_cdb(const str * filename)89 static struct cdb* open_cdb(const str* filename)
90 {
91   int fd;
92   struct cdb* c;
93   if ((c = malloc(sizeof *c)) == 0) return 0;
94   fd = open(filename->s, O_RDONLY);
95   cdb_init(c, fd);
96   if (!dict_add(&cdb_files, filename, c)) return 0;
97   return c;
98 }
99 
lower(str * s)100 static int lower(str* s)
101 {
102   str_lower(s);
103   return 1;
104 }
105 
106 static dict text_files;
load_text(const str * filename)107 static dict* load_text(const str* filename)
108 {
109   dict* d;
110   if ((d = malloc(sizeof *d)) == 0) return 0;
111   memset(d, 0, sizeof *d);
112   if (!dict_load_list(d, filename->s, 1, lower)) return 0;
113   if (!dict_add(&text_files, filename, d)) return 0;
114   return d;
115 }
116 
is_special(const str * pattern)117 static int is_special(const str* pattern)
118 {
119   long len = pattern->len;
120   return len > 5 &&
121     pattern->s[0] == '[' &&
122     pattern->s[1] == '[' &&
123     pattern->s[len-2] == ']' &&
124     pattern->s[len-1] == ']';
125 }
126 
is_cdb(const str * filename)127 static int is_cdb(const str* filename)
128 {
129   const char* end = filename->s + filename->len;
130   return filename->len > 4 &&
131     end[-4] == '.' &&
132     end[-3] == 'c' &&
133     end[-2] == 'd' &&
134     end[-1] == 'b';
135 }
136 
137 static str filename;
extract_filename(const str * pattern)138 static int extract_filename(const str* pattern)
139 {
140   if (!is_special(pattern)) return 0;
141   if (pattern->s[2] == '@')
142     str_copyb(&filename, pattern->s+3, pattern->len-5);
143   else
144     str_copyb(&filename, pattern->s+2, pattern->len-4);
145   return 1;
146 }
147 
try_load(struct pattern * pattern)148 static int try_load(struct pattern* pattern)
149 {
150   if (extract_filename(&pattern->pattern)) {
151     if (is_cdb(&filename))
152       return (pattern->cdb = open_cdb(&filename)) != 0;
153     else
154       return (pattern->file = load_text(&filename)) != 0;
155   }
156   return 1;
157 }
158 
apply_environment(const str * s)159 static void apply_environment(const str* s)
160 {
161   unsigned i;
162   unsigned len;
163   for (i = 0; i < s->len; i += len + 1) {
164     len = strlen(s->s + i);
165     session_putenv(s->s + i);
166   }
167 }
168 
169 /* file parsing ************************************************************ */
isoctal(int ch)170 static int isoctal(int ch) { return ch >= '0' && ch <= '8'; }
171 
parse_uint(const char * ptr,char sep,unsigned long * out)172 static const char* parse_uint(const char* ptr, char sep,
173 			      unsigned long* out)
174 {
175   for (*out = 0; *ptr != 0 && *ptr != sep; ++ptr) {
176     if (*ptr >= '0' && *ptr <= '9')
177       *out = (*out * 10) + (*ptr - '0');
178     else
179       return 0;
180   }
181   return ptr;
182 }
183 
parse_char(const char * ptr,char * out)184 static const char* parse_char(const char* ptr, char* out)
185 {
186   int o;
187   switch (*ptr) {
188   case 0: return ptr;
189   case '\\':
190     switch (*++ptr) {
191     case 'n': *out = '\n'; break;
192     case '0': case '1': case '2': case '3':
193     case '4': case '5': case '6': case '7':
194       o = *ptr - '0';
195       if (isoctal(ptr[1])) {
196 	o = (o << 3) | (*++ptr - '0');
197 	if (isoctal(ptr[1]))
198 	  o = (o << 3) | (*++ptr - '0');
199       }
200       *out = 0;
201       break;
202     default: *out = *ptr;
203     }
204     break;
205   default: *out = *ptr;
206   }
207   return ptr + 1;
208 }
209 
parse_str(const char * ptr,char sep,str * out)210 static const char* parse_str(const char* ptr, char sep, str* out)
211 {
212   char ch = 0;
213   /* str_truncate(out, 0); */
214   for (;;) {
215     if (*ptr == sep || *ptr == NUL) return ptr;
216     ptr = parse_char(ptr, &ch);
217     str_catc(out, ch);
218   }
219   return ptr;
220 }
221 
parse_pattern(const char * ptr,char sep,struct pattern * out)222 static const char* parse_pattern(const char* ptr, char sep,
223 				 struct pattern* out)
224 {
225   while (*ptr != sep && *ptr == '!') {
226     out->negated = !out->negated;
227     ++ptr;
228   }
229   return parse_str(ptr, sep, &out->pattern);
230 }
231 
parse_env(const char * ptr,str * out)232 static void parse_env(const char* ptr, str* out)
233 {
234   while (ptr && *ptr != 0)
235     if ((ptr = parse_str(ptr, ',', out)) != 0) {
236       str_catc(out, 0);
237       if (*ptr == ',') ++ptr;
238     }
239 }
240 
iscode(char ch)241 static int iscode(char ch)
242 {
243   return ch == 'k' || ch == 'K' || ch == 'd' || ch == 'z' || ch == 'p' || ch == 'n' || ch == '&';
244 }
245 
add(const char * l)246 static const response* add(const char* l)
247 {
248   struct rule* r;
249 
250   if (!iscode(*l)) return 0;
251   r = alloc_rule();
252   r->code = *l++;
253 
254   if ((l = parse_pattern(l, ':', &r->sender)) != 0 && *l == ':')
255     if ((l = parse_pattern(l+1, ':', &r->recipient)) != 0 && *l == ':')
256       if ((l = parse_str(l+1, ':', &r->response)) != 0 && *l == ':')
257 	if ((l = parse_uint(l+1, ':', &r->databytes)) != 0)
258 	  if (*l == ':'
259 	      && (l = parse_str(l+1, ':', &r->relayclient)) != 0
260 	      && *l == ':')
261 	    parse_env(l+1, &r->environment);
262 
263   if (l == 0) return &resp_syntax;
264   append_rule(r);
265 
266   /* Pre-load text files and pre-open CDB files */
267   if (!try_load(&r->sender)) return &resp_erropenref;
268   if (!try_load(&r->recipient)) return &resp_erropenref;
269   return 0;
270 }
271 
272 static int loaded = 0;
273 
init(void)274 static const response* init(void)
275 {
276   const char* path;
277   str rule = {0,0,0};
278   ibuf in;
279   const response* r;
280 
281   if ((path = getenv("MAILRULES")) == 0) return 0;
282   loaded = 1;
283 
284   if (!ibuf_open(&in, path, 0)) return &resp_erropen;
285   while (ibuf_getstr(&in, &rule, LF)) {
286     str_strip(&rule);
287     if (rule.len == 0) continue;
288     if (rule.s[0] == ':') {
289       switch (rule.s[1]) {
290       case 's': current_rules = &sender_rules; break;
291       case 'r': current_rules = &recip_rules; break;
292       default: return &resp_syntax;
293       }
294     }
295     else if ((r = add(rule.s)) != 0)
296       return r;
297   }
298   ibuf_close(&in);
299   str_free(&rule);
300   return 0;
301 }
302 
reset(void)303 static const response* reset(void)
304 {
305   if (loaded) {
306     session_resetenv();
307     session_delnum("maxdatabytes");
308   }
309   return 0;
310 }
311 
312 /* rule application ******************************************************** */
matches(const struct pattern * pattern,const str * addr,const str * atdomain)313 static int matches(const struct pattern* pattern,
314 		   const str* addr, const str* atdomain)
315 {
316   static str domain;
317   int result;
318   if (pattern->cdb != 0) {
319     if (pattern->pattern.s[2] == '@') {
320       result = (atdomain->len == 0)
321 	? 0
322 	: cdb_find(pattern->cdb, atdomain->s+1, atdomain->len-1) != 0;
323     }
324     else {
325       result = (cdb_find(pattern->cdb, addr->s, addr->len) != 0) ?
326 	1 :
327 	cdb_find(pattern->cdb, atdomain->s, atdomain->len) != 0;
328     }
329   }
330   else if (pattern->file != 0) {
331     if (pattern->pattern.s[2] == '@') {
332       if (atdomain->len > 0) {
333 	str_copyb(&domain, atdomain->s+1, atdomain->len-1);
334 	result = dict_get(pattern->file, &domain) != 0;
335       }
336       else
337 	result = 0;
338     }
339     else {
340       result = (dict_get(pattern->file, addr) != 0) ?
341 	1 :
342 	dict_get(pattern->file, atdomain) != 0;
343     }
344   }
345   else
346     result = str_case_glob(addr, &pattern->pattern);
347   if (pattern->negated)
348     result = !result;
349   return result;
350 }
351 
build_response(int type,const str * message)352 static const response* build_response(int type, const str* message)
353 {
354   static response resp;
355   unsigned code;
356   const char* defmsg = NULL;
357 
358   switch (type) {
359   case 'p': return 0;
360   case 'n': return 0;
361   case 'k': code = 250; break;
362   case 'K': code = 250 | RESPONSE_FINAL; break;
363   case 'd': code = 553; defmsg = "Rejected"; break;
364   case 'z': code = 451; defmsg = "Deferred"; break;
365   default:  code = 451; defmsg = "Temporary failure"; break;
366   }
367 
368   resp.number = code;
369   resp.message = (message->len == 0) ? defmsg : message->s;
370   return &resp;
371 }
372 
apply_rule(const struct rule * rule)373 static const response* apply_rule(const struct rule* rule)
374 {
375   const response* resp;
376   unsigned long maxdatabytes;
377   resp = build_response(rule->code, &rule->response);
378   apply_environment(&rule->environment);
379   maxdatabytes = session_getnum("maxdatabytes", ~0UL);
380   if (maxdatabytes == 0
381       || (rule->databytes > 0
382 	  && maxdatabytes > rule->databytes))
383     session_setnum("maxdatabytes", rule->databytes);
384   return resp;
385 }
386 
copy_addr(const str * addr,str * saved,str * domain)387 static void copy_addr(const str* addr,
388 		      str* saved, str* domain)
389 {
390   int at;
391   str_copy(saved, addr);
392   str_lower(saved);
393   if ((at = str_findlast(saved, '@')) != -1)
394     str_copyb(domain, saved->s + at, saved->len - at);
395   else
396     str_truncate(domain, 0);
397 }
398 
399 static str saved_sender;
400 static str sender_domain;
401 
402 static str saved_recip;
403 static str recip_domain;
404 
rule_matches(const struct rule * rule,int is_recip)405 static int rule_matches(const struct rule* rule, int is_recip)
406 {
407   return matches(&rule->sender, &saved_sender, &sender_domain)
408     && (!is_recip || matches(&rule->recipient, &saved_recip, &recip_domain));
409 }
410 
run_rules(const struct rule * rule,int is_recip,str * addr,str * save_addr,str * save_domain)411 static const response* run_rules(const struct rule* rule, int is_recip, str* addr, str* save_addr, str* save_domain)
412 {
413   int prev_and;
414   if (!loaded)
415     return NULL;
416   copy_addr(addr, save_addr, save_domain);
417 
418   for (prev_and = 1; rule != NULL; rule = rule->next) {
419     if (rule->code == '&') {
420       prev_and = prev_and && rule_matches(rule, is_recip);
421     }
422     else if (prev_and && rule_matches(rule, is_recip)) {
423       const response* r = apply_rule(rule);
424       if (is_recip)
425         str_cat(addr, &rule->relayclient);
426       if (rule->code != 'n')
427         return r;
428     }
429     else
430       prev_and = 1;
431   }
432   return NULL;
433 }
434 
validate_sender(str * sender,str * params)435 static const response* validate_sender(str* sender, str* params)
436 {
437   return run_rules(sender_rules, 0, sender, &saved_sender, &sender_domain);
438   (void)params;
439 }
440 
validate_recipient(str * recipient,str * params)441 static const response* validate_recipient(str* recipient, str* params)
442 {
443   return run_rules(recip_rules, 1, recipient, &saved_recip, &recip_domain);
444   (void)params;
445 }
446 
447 struct plugin plugin = {
448   .version = PLUGIN_VERSION,
449   .init = init,
450   .reset = reset,
451   .sender = validate_sender,
452   .recipient = validate_recipient,
453 };
454