| 1 | /************************************************* |
| 2 | * Exim - an Internet mail transport agent * |
| 3 | *************************************************/ |
| 4 | |
| 5 | /* Copyright (c) University of Cambridge 1995 - 2009 */ |
| 6 | /* See the file NOTICE for conditions of use and distribution. */ |
| 7 | |
| 8 | |
| 9 | /* Code for the filter test function. */ |
| 10 | |
| 11 | #include "exim.h" |
| 12 | |
| 13 | |
| 14 | |
| 15 | /************************************************* |
| 16 | * Read message and set body/size variables * |
| 17 | *************************************************/ |
| 18 | |
| 19 | /* We have to read the remainder of the message in order to find its size, so |
| 20 | we can set up the message_body variables at the same time (in normal use, the |
| 21 | message_body variables are not set up unless needed). The reading code is |
| 22 | written out here rather than having options in read_message_data, in order to |
| 23 | keep that function as efficient as possible. (Later: this function is now |
| 24 | global because it is also used by the -bem testing option.) Handling |
| 25 | message_body_end is somewhat more tedious. Pile it all into a circular buffer |
| 26 | and sort out at the end. |
| 27 | |
| 28 | Arguments: |
| 29 | dot_ended TRUE if message already terminated by '.' |
| 30 | |
| 31 | Returns: nothing |
| 32 | */ |
| 33 | |
| 34 | void |
| 35 | read_message_body(BOOL dot_ended) |
| 36 | { |
| 37 | register int ch; |
| 38 | int body_len, body_end_len, header_size; |
| 39 | uschar *s; |
| 40 | |
| 41 | message_body = store_malloc(message_body_visible + 1); |
| 42 | message_body_end = store_malloc(message_body_visible + 1); |
| 43 | s = message_body_end; |
| 44 | body_len = 0; |
| 45 | body_linecount = 0; |
| 46 | header_size = message_size; |
| 47 | |
| 48 | if (!dot_ended && !feof(stdin)) |
| 49 | { |
| 50 | if (!f.dot_ends) |
| 51 | { |
| 52 | while ((ch = getc(stdin)) != EOF) |
| 53 | { |
| 54 | if (ch == 0) body_zerocount++; |
| 55 | if (ch == '\n') body_linecount++; |
| 56 | if (body_len < message_body_visible) message_body[body_len++] = ch; |
| 57 | *s++ = ch; |
| 58 | if (s > message_body_end + message_body_visible) s = message_body_end; |
| 59 | message_size++; |
| 60 | } |
| 61 | } |
| 62 | else |
| 63 | { |
| 64 | int ch_state = 1; |
| 65 | while ((ch = getc(stdin)) != EOF) |
| 66 | { |
| 67 | if (ch == 0) body_zerocount++; |
| 68 | switch (ch_state) |
| 69 | { |
| 70 | case 0: /* Normal state */ |
| 71 | if (ch == '\n') { body_linecount++; ch_state = 1; } |
| 72 | break; |
| 73 | |
| 74 | case 1: /* After "\n" */ |
| 75 | if (ch == '.') |
| 76 | { |
| 77 | ch_state = 2; |
| 78 | continue; |
| 79 | } |
| 80 | if (ch != '\n') ch_state = 0; |
| 81 | break; |
| 82 | |
| 83 | case 2: /* After "\n." */ |
| 84 | if (ch == '\n') goto READ_END; |
| 85 | if (body_len < message_body_visible) message_body[body_len++] = '.'; |
| 86 | *s++ = '.'; |
| 87 | if (s > message_body_end + message_body_visible) |
| 88 | s = message_body_end; |
| 89 | message_size++; |
| 90 | ch_state = 0; |
| 91 | break; |
| 92 | } |
| 93 | if (body_len < message_body_visible) message_body[body_len++] = ch; |
| 94 | *s++ = ch; |
| 95 | if (s > message_body_end + message_body_visible) s = message_body_end; |
| 96 | message_size++; |
| 97 | } |
| 98 | READ_END: ch = ch; /* Some compilers don't like null statements */ |
| 99 | } |
| 100 | if (s == message_body_end || s[-1] != '\n') body_linecount++; |
| 101 | } |
| 102 | |
| 103 | message_body[body_len] = 0; |
| 104 | message_body_size = message_size - header_size; |
| 105 | |
| 106 | /* body_len stops at message_body_visible; it if got there, we may have |
| 107 | wrapped round in message_body_end. */ |
| 108 | |
| 109 | if (body_len >= message_body_visible) |
| 110 | { |
| 111 | int below = s - message_body_end; |
| 112 | int above = message_body_visible - below; |
| 113 | if (above > 0) |
| 114 | { |
| 115 | uschar *temp = store_get(below); |
| 116 | memcpy(temp, message_body_end, below); |
| 117 | memmove(message_body_end, s+1, above); |
| 118 | memcpy(message_body_end + above, temp, below); |
| 119 | s = message_body_end + message_body_visible; |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | *s = 0; |
| 124 | body_end_len = s - message_body_end; |
| 125 | |
| 126 | /* Convert newlines and nulls in the body variables to spaces */ |
| 127 | |
| 128 | while (body_len > 0) |
| 129 | { |
| 130 | if (message_body[--body_len] == '\n' || message_body[body_len] == 0) |
| 131 | message_body[body_len] = ' '; |
| 132 | } |
| 133 | |
| 134 | while (body_end_len > 0) |
| 135 | { |
| 136 | if (message_body_end[--body_end_len] == '\n' || |
| 137 | message_body_end[body_end_len] == 0) |
| 138 | message_body_end[body_end_len] = ' '; |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | |
| 143 | |
| 144 | /************************************************* |
| 145 | * Test a mail filter * |
| 146 | *************************************************/ |
| 147 | |
| 148 | /* This is called when exim is run with the -bf option. At this point it is |
| 149 | running under an unprivileged uid/gid. A test message's headers have been read |
| 150 | into store, and the body of the message is still accessible on the standard |
| 151 | input if this is the first time this function has been called. It may be called |
| 152 | twice if both system and user filters are being tested. |
| 153 | |
| 154 | Argument: |
| 155 | fd an fd containing the filter file |
| 156 | filename the name of the filter file |
| 157 | is_system TRUE if testing is to be as a system filter |
| 158 | dot_ended TRUE if message already terminated by '.' |
| 159 | |
| 160 | Returns: TRUE if no errors |
| 161 | */ |
| 162 | |
| 163 | BOOL |
| 164 | filter_runtest(int fd, uschar *filename, BOOL is_system, BOOL dot_ended) |
| 165 | { |
| 166 | int rc, filter_type; |
| 167 | BOOL yield; |
| 168 | struct stat statbuf; |
| 169 | address_item *generated = NULL; |
| 170 | uschar *error, *filebuf; |
| 171 | |
| 172 | /* Read the filter file into store as will be done by the router in a real |
| 173 | case. */ |
| 174 | |
| 175 | if (fstat(fd, &statbuf) != 0) |
| 176 | { |
| 177 | printf("exim: failed to get size of %s: %s\n", filename, strerror(errno)); |
| 178 | return FALSE; |
| 179 | } |
| 180 | |
| 181 | filebuf = store_get(statbuf.st_size + 1); |
| 182 | rc = read(fd, filebuf, statbuf.st_size); |
| 183 | (void)close(fd); |
| 184 | |
| 185 | if (rc != statbuf.st_size) |
| 186 | { |
| 187 | printf("exim: error while reading %s: %s\n", filename, strerror(errno)); |
| 188 | return FALSE; |
| 189 | } |
| 190 | |
| 191 | filebuf[statbuf.st_size] = 0; |
| 192 | |
| 193 | /* Check the filter type. User filters start with "# Exim filter" or "# Sieve |
| 194 | filter". If the filter type is not recognized, the file is treated as an |
| 195 | ordinary .forward file. System filters do not need the "# Exim filter" in order |
| 196 | to be recognized as Exim filters. */ |
| 197 | |
| 198 | filter_type = rda_is_filter(filebuf); |
| 199 | if (is_system && filter_type == FILTER_FORWARD) filter_type = FILTER_EXIM; |
| 200 | |
| 201 | printf("Testing %s file \"%s\"\n\n", |
| 202 | (filter_type == FILTER_EXIM)? "Exim filter" : |
| 203 | (filter_type == FILTER_SIEVE)? "Sieve filter" : |
| 204 | "forward file", |
| 205 | filename); |
| 206 | |
| 207 | /* Handle a plain .forward file */ |
| 208 | |
| 209 | if (filter_type == FILTER_FORWARD) |
| 210 | { |
| 211 | yield = parse_forward_list(filebuf, |
| 212 | RDO_REWRITE, |
| 213 | &generated, /* for generated addresses */ |
| 214 | &error, /* for errors */ |
| 215 | deliver_domain, /* incoming domain for \name */ |
| 216 | NULL, /* no check on includes */ |
| 217 | NULL); /* fail on syntax errors */ |
| 218 | |
| 219 | switch(yield) |
| 220 | { |
| 221 | case FF_FAIL: |
| 222 | printf("exim: forward file contains \":fail:\"\n"); |
| 223 | break; |
| 224 | |
| 225 | case FF_BLACKHOLE: |
| 226 | printf("exim: forwardfile contains \":blackhole:\"\n"); |
| 227 | break; |
| 228 | |
| 229 | case FF_ERROR: |
| 230 | printf("exim: error in forward file: %s\n", error); |
| 231 | return FALSE; |
| 232 | } |
| 233 | |
| 234 | if (generated == NULL) |
| 235 | printf("exim: no addresses generated from forward file\n"); |
| 236 | |
| 237 | else |
| 238 | { |
| 239 | printf("exim: forward file generated:\n"); |
| 240 | while (generated != NULL) |
| 241 | { |
| 242 | printf(" %s\n", generated->address); |
| 243 | generated = generated->next; |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | return TRUE; |
| 248 | } |
| 249 | |
| 250 | /* For a filter, set up the message_body variables and the message size if this |
| 251 | is the first time this function has been called. */ |
| 252 | |
| 253 | if (message_body == NULL) read_message_body(dot_ended); |
| 254 | |
| 255 | /* Now pass the filter file to the function that interprets it. Because |
| 256 | filter_test is not FILTER_NONE, the interpreter will output comments about what |
| 257 | it is doing. No need to clean up store. Indeed, we must not, because we may be |
| 258 | testing a system filter that is going to be followed by a user filter test. */ |
| 259 | |
| 260 | if (is_system) |
| 261 | { |
| 262 | f.system_filtering = TRUE; |
| 263 | f.enable_dollar_recipients = TRUE; /* Permit $recipients in system filter */ |
| 264 | yield = filter_interpret |
| 265 | (filebuf, |
| 266 | RDO_DEFER|RDO_FAIL|RDO_FILTER|RDO_FREEZE|RDO_REWRITE, &generated, &error); |
| 267 | f.enable_dollar_recipients = FALSE; |
| 268 | f.system_filtering = FALSE; |
| 269 | } |
| 270 | else |
| 271 | { |
| 272 | yield = (filter_type == FILTER_SIEVE)? |
| 273 | sieve_interpret(filebuf, RDO_REWRITE, NULL, NULL, NULL, NULL, &generated, &error) |
| 274 | : |
| 275 | filter_interpret(filebuf, RDO_REWRITE, &generated, &error); |
| 276 | } |
| 277 | |
| 278 | return yield != FF_ERROR; |
| 279 | } |
| 280 | |
| 281 | /* End of filtertest.c */ |