Commit | Line | Data |
---|---|---|
0756eb3c PH |
1 | /************************************************* |
2 | * Exim - an Internet mail transport agent * | |
3 | *************************************************/ | |
4 | ||
f9ba5e22 | 5 | /* Copyright (c) University of Cambridge 1995 - 2018 */ |
0756eb3c PH |
6 | /* See the file NOTICE for conditions of use and distribution. */ |
7 | ||
8 | #include "../exim.h" | |
9 | #include "rf_functions.h" | |
10 | #include "queryprogram.h" | |
11 | ||
12 | ||
13 | ||
14 | /* Options specific to the queryprogram router. */ | |
15 | ||
16 | optionlist queryprogram_router_options[] = { | |
17 | { "*expand_command_group", opt_bool | opt_hidden, | |
13a4b4c1 | 18 | OPT_OFF(queryprogram_router_options_block, expand_cmd_gid) }, |
0756eb3c | 19 | { "*expand_command_user", opt_bool | opt_hidden, |
13a4b4c1 | 20 | OPT_OFF(queryprogram_router_options_block, expand_cmd_uid) }, |
0756eb3c | 21 | { "*set_command_group", opt_bool | opt_hidden, |
13a4b4c1 | 22 | OPT_OFF(queryprogram_router_options_block, cmd_gid_set) }, |
0756eb3c | 23 | { "*set_command_user", opt_bool | opt_hidden, |
13a4b4c1 | 24 | OPT_OFF(queryprogram_router_options_block, cmd_uid_set) }, |
0756eb3c | 25 | { "command", opt_stringptr, |
13a4b4c1 | 26 | OPT_OFF(queryprogram_router_options_block, command) }, |
0756eb3c | 27 | { "command_group",opt_expand_gid, |
13a4b4c1 | 28 | OPT_OFF(queryprogram_router_options_block, cmd_gid) }, |
0756eb3c | 29 | { "command_user", opt_expand_uid, |
13a4b4c1 | 30 | OPT_OFF(queryprogram_router_options_block, cmd_uid) }, |
0756eb3c | 31 | { "current_directory", opt_stringptr, |
13a4b4c1 | 32 | OPT_OFF(queryprogram_router_options_block, current_directory) }, |
0756eb3c | 33 | { "timeout", opt_time, |
13a4b4c1 | 34 | OPT_OFF(queryprogram_router_options_block, timeout) } |
0756eb3c PH |
35 | }; |
36 | ||
37 | /* Size of the options list. An extern variable has to be used so that its | |
38 | address can appear in the tables drtables.c. */ | |
39 | ||
40 | int queryprogram_router_options_count = | |
41 | sizeof(queryprogram_router_options)/sizeof(optionlist); | |
42 | ||
d185889f JH |
43 | |
44 | #ifdef MACRO_PREDEF | |
45 | ||
46 | /* Dummy entries */ | |
47 | queryprogram_router_options_block queryprogram_router_option_defaults = {0}; | |
48 | void queryprogram_router_init(router_instance *rblock) {} | |
49 | int queryprogram_router_entry(router_instance *rblock, address_item *addr, | |
50 | struct passwd *pw, int verify, address_item **addr_local, | |
51 | address_item **addr_remote, address_item **addr_new, | |
cab0c277 | 52 | address_item **addr_succeed) {return 0;} |
d185889f JH |
53 | |
54 | #else /*!MACRO_PREDEF*/ | |
55 | ||
56 | ||
0756eb3c PH |
57 | /* Default private options block for the queryprogram router. */ |
58 | ||
59 | queryprogram_router_options_block queryprogram_router_option_defaults = { | |
60 | NULL, /* command */ | |
61 | 60*60, /* timeout */ | |
62 | (uid_t)(-1), /* cmd_uid */ | |
63 | (gid_t)(-1), /* cmd_gid */ | |
64 | FALSE, /* cmd_uid_set */ | |
65 | FALSE, /* cmd_gid_set */ | |
66 | US"/", /* current_directory */ | |
67 | NULL, /* expand_cmd_gid */ | |
68 | NULL /* expand_cmd_uid */ | |
69 | }; | |
70 | ||
71 | ||
72 | ||
73 | /************************************************* | |
74 | * Initialization entry point * | |
75 | *************************************************/ | |
76 | ||
77 | /* Called for each instance, after its options have been read, to enable | |
78 | consistency checks to be done, or anything else that needs to be set up. */ | |
79 | ||
80 | void | |
81 | queryprogram_router_init(router_instance *rblock) | |
82 | { | |
83 | queryprogram_router_options_block *ob = | |
84 | (queryprogram_router_options_block *)(rblock->options_block); | |
85 | ||
86 | /* A command must be given */ | |
87 | ||
88 | if (ob->command == NULL) | |
89 | log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " | |
90 | "a command specification is required", rblock->name); | |
91 | ||
92 | /* A uid/gid must be supplied */ | |
93 | ||
94 | if (!ob->cmd_uid_set && ob->expand_cmd_uid == NULL) | |
95 | log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " | |
96 | "command_user must be specified", rblock->name); | |
97 | } | |
98 | ||
99 | ||
100 | ||
101 | /************************************************* | |
102 | * Process a set of generated new addresses * | |
103 | *************************************************/ | |
104 | ||
105 | /* This function sets up a set of newly generated child addresses and puts them | |
106 | on the new address chain. | |
107 | ||
108 | Arguments: | |
109 | rblock router block | |
110 | addr_new new address chain | |
111 | addr original address | |
112 | generated list of generated addresses | |
113 | addr_prop the propagated data block, containing errors_to, | |
114 | header change stuff, and address_data | |
115 | ||
116 | Returns: nothing | |
117 | */ | |
118 | ||
119 | static void | |
120 | add_generated(router_instance *rblock, address_item **addr_new, | |
121 | address_item *addr, address_item *generated, | |
122 | address_item_propagated *addr_prop) | |
123 | { | |
124 | while (generated != NULL) | |
125 | { | |
7eb0e5d2 | 126 | BOOL ignore_error = addr->prop.ignore_error; |
0756eb3c | 127 | address_item *next = generated; |
7eb0e5d2 | 128 | |
0756eb3c PH |
129 | generated = next->next; |
130 | ||
131 | next->parent = addr; | |
d43cbe25 | 132 | next->prop = *addr_prop; |
a5853d7c | 133 | next->prop.ignore_error = next->prop.ignore_error || ignore_error; |
0756eb3c PH |
134 | next->start_router = rblock->redirect_router; |
135 | ||
136 | next->next = *addr_new; | |
137 | *addr_new = next; | |
138 | ||
82f90600 | 139 | if (addr->child_count == USHRT_MAX) |
4362ff0d | 140 | log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s router generated more than %d " |
82f90600 | 141 | "child addresses for <%s>", rblock->name, USHRT_MAX, addr->address); |
0756eb3c PH |
142 | addr->child_count++; |
143 | ||
144 | DEBUG(D_route) | |
145 | debug_printf("%s router generated %s\n", rblock->name, next->address); | |
146 | } | |
147 | } | |
148 | ||
149 | ||
150 | ||
151 | ||
152 | /************************************************* | |
153 | * Main entry point * | |
154 | *************************************************/ | |
155 | ||
156 | /* See local README for interface details. This router returns: | |
157 | ||
158 | DECLINE | |
159 | . DECLINE returned | |
160 | . self = DECLINE | |
161 | ||
162 | PASS | |
163 | . PASS returned | |
164 | . timeout of host lookup and pass_on_timeout set | |
165 | . self = PASS | |
166 | ||
167 | DEFER | |
168 | . verifying the errors address caused a deferment or a big disaster such | |
169 | as an expansion failure (rf_get_errors_address) | |
170 | . expanding a headers_{add,remove} string caused a deferment or another | |
171 | expansion error (rf_get_munge_headers) | |
172 | . a problem in rf_get_transport: no transport when one is needed; | |
173 | failed to expand dynamic transport; failed to find dynamic transport | |
174 | . bad lookup type | |
175 | . problem looking up host (rf_lookup_hostlist) | |
176 | . self = DEFER or FREEZE | |
177 | . failure to set up uid/gid for running the command | |
178 | . failure of transport_set_up_command: too many arguments, expansion fail | |
179 | . failure to create child process | |
180 | . child process crashed or timed out or didn't return data | |
181 | . :defer: in data | |
182 | . DEFER or FREEZE returned | |
183 | . problem in redirection data | |
184 | . unknown transport name or trouble expanding router transport | |
185 | ||
186 | FAIL | |
187 | . :fail: in data | |
188 | . FAIL returned | |
189 | . self = FAIL | |
190 | ||
191 | OK | |
192 | . address added to addr_local or addr_remote for delivery | |
193 | . new addresses added to addr_new | |
194 | */ | |
195 | ||
196 | int | |
197 | queryprogram_router_entry( | |
198 | router_instance *rblock, /* data for this instantiation */ | |
199 | address_item *addr, /* address we are working on */ | |
200 | struct passwd *pw, /* passwd entry after check_local_user */ | |
fd6de02e | 201 | int verify, /* v_none/v_recipient/v_sender/v_expn */ |
0756eb3c PH |
202 | address_item **addr_local, /* add it to this if it's local */ |
203 | address_item **addr_remote, /* add it to this if it's remote */ | |
204 | address_item **addr_new, /* put new addresses on here */ | |
205 | address_item **addr_succeed) /* put old address here on success */ | |
206 | { | |
207 | int fd_in, fd_out, len, rc; | |
208 | pid_t pid; | |
209 | struct passwd *upw = NULL; | |
210 | uschar buffer[1024]; | |
55414b25 | 211 | const uschar **argvptr; |
0756eb3c PH |
212 | uschar *rword, *rdata, *s; |
213 | address_item_propagated addr_prop; | |
214 | queryprogram_router_options_block *ob = | |
215 | (queryprogram_router_options_block *)(rblock->options_block); | |
216 | uschar *current_directory = ob->current_directory; | |
217 | ugid_block ugid; | |
59e82a2a PH |
218 | uid_t curr_uid = getuid(); |
219 | gid_t curr_gid = getgid(); | |
0756eb3c PH |
220 | uid_t uid = ob->cmd_uid; |
221 | gid_t gid = ob->cmd_gid; | |
59e82a2a PH |
222 | uid_t *puid = &uid; |
223 | gid_t *pgid = &gid; | |
0756eb3c PH |
224 | |
225 | DEBUG(D_route) debug_printf("%s router called for %s: domain = %s\n", | |
226 | rblock->name, addr->address, addr->domain); | |
227 | ||
228 | ugid.uid_set = ugid.gid_set = FALSE; | |
229 | ||
230 | /* Set up the propagated data block with the current address_data and the | |
231 | errors address and extra header stuff. */ | |
232 | ||
806c3df9 | 233 | bzero(&addr_prop, sizeof(addr_prop)); |
0756eb3c | 234 | addr_prop.address_data = deliver_address_data; |
b4f579d1 | 235 | tree_dup((tree_node **)&addr_prop.variables, addr->prop.variables); |
0756eb3c | 236 | |
d43cbe25 | 237 | rc = rf_get_errors_address(addr, rblock, verify, &addr_prop.errors_address); |
0756eb3c PH |
238 | if (rc != OK) return rc; |
239 | ||
d43cbe25 JH |
240 | rc = rf_get_munge_headers(addr, rblock, &addr_prop.extra_headers, |
241 | &addr_prop.remove_headers); | |
0756eb3c PH |
242 | if (rc != OK) return rc; |
243 | ||
67e87fcf JH |
244 | #ifdef EXPERIMENTAL_SRS |
245 | addr_prop.srs_sender = NULL; | |
246 | #endif | |
247 | ||
0756eb3c PH |
248 | /* Get the fixed or expanded uid under which the command is to run |
249 | (initialization ensures that one or the other is set). */ | |
250 | ||
251 | if (!ob->cmd_uid_set) | |
252 | { | |
253 | if (!route_find_expanded_user(ob->expand_cmd_uid, rblock->name, US"router", | |
254 | &upw, &uid, &(addr->message))) | |
255 | return DEFER; | |
256 | } | |
257 | ||
258 | /* Get the fixed or expanded gid, or take the gid from the passwd entry. */ | |
259 | ||
260 | if (!ob->cmd_gid_set) | |
261 | { | |
262 | if (ob->expand_cmd_gid != NULL) | |
263 | { | |
264 | if (route_find_expanded_group(ob->expand_cmd_gid, rblock->name, | |
265 | US"router", &gid, &(addr->message))) | |
266 | return DEFER; | |
267 | } | |
268 | else if (upw != NULL) | |
269 | { | |
270 | gid = upw->pw_gid; | |
271 | } | |
272 | else | |
273 | { | |
274 | addr->message = string_sprintf("command_user set without command_group " | |
275 | "for %s router", rblock->name); | |
276 | return DEFER; | |
277 | } | |
278 | } | |
279 | ||
59e82a2a | 280 | DEBUG(D_route) debug_printf("requires uid=%ld gid=%ld current_directory=%s\n", |
0756eb3c PH |
281 | (long int)uid, (long int)gid, current_directory); |
282 | ||
59e82a2a PH |
283 | /* If we are not running as root, we will not be able to change uid/gid. */ |
284 | ||
285 | if (curr_uid != root_uid && (uid != curr_uid || gid != curr_gid)) | |
286 | { | |
287 | DEBUG(D_route) | |
288 | { | |
289 | debug_printf("not running as root: cannot change uid/gid\n"); | |
290 | debug_printf("subprocess will run with uid=%ld gid=%ld\n", | |
291 | (long int)curr_uid, (long int)curr_gid); | |
292 | } | |
293 | puid = pgid = NULL; | |
294 | } | |
295 | ||
296 | /* Set up the command to run */ | |
297 | ||
0756eb3c PH |
298 | if (!transport_set_up_command(&argvptr, /* anchor for arg list */ |
299 | ob->command, /* raw command */ | |
300 | TRUE, /* expand the arguments */ | |
301 | 0, /* not relevant when... */ | |
302 | NULL, /* no transporting address */ | |
303 | US"queryprogram router", /* for error messages */ | |
304 | &(addr->message))) /* where to put error message */ | |
305 | { | |
306 | return DEFER; | |
307 | } | |
308 | ||
309 | /* Create the child process, making it a group leader. */ | |
310 | ||
eb24befc JH |
311 | if ((pid = child_open_uid(argvptr, NULL, 0077, puid, pgid, &fd_in, &fd_out, |
312 | current_directory, TRUE, US"queryprogram-cmd")) < 0) | |
0756eb3c PH |
313 | { |
314 | addr->message = string_sprintf("%s router couldn't create child process: %s", | |
315 | rblock->name, strerror(errno)); | |
316 | return DEFER; | |
317 | } | |
318 | ||
319 | /* Nothing is written to the standard input. */ | |
320 | ||
f1e894f3 | 321 | (void)close(fd_in); |
0756eb3c PH |
322 | |
323 | /* Wait for the process to finish, applying the timeout, and inspect its return | |
324 | code. */ | |
325 | ||
326 | if ((rc = child_close(pid, ob->timeout)) != 0) | |
327 | { | |
328 | if (rc > 0) | |
329 | addr->message = string_sprintf("%s router: command returned non-zero " | |
330 | "code %d", rblock->name, rc); | |
331 | ||
332 | else if (rc == -256) | |
333 | { | |
334 | addr->message = string_sprintf("%s router: command timed out", | |
335 | rblock->name); | |
336 | killpg(pid, SIGKILL); /* Kill the whole process group */ | |
337 | } | |
338 | ||
339 | else if (rc == -257) | |
340 | addr->message = string_sprintf("%s router: wait() failed: %s", | |
341 | rblock->name, strerror(errno)); | |
342 | ||
343 | else | |
344 | addr->message = string_sprintf("%s router: command killed by signal %d", | |
345 | rblock->name, -rc); | |
346 | ||
347 | return DEFER; | |
348 | } | |
349 | ||
350 | /* Read the pipe to get the command's output, and then close it. */ | |
351 | ||
352 | len = read(fd_out, buffer, sizeof(buffer) - 1); | |
f1e894f3 | 353 | (void)close(fd_out); |
0756eb3c PH |
354 | |
355 | /* Failure to return any data is an error. */ | |
356 | ||
357 | if (len <= 0) | |
358 | { | |
359 | addr->message = string_sprintf("%s router: command failed to return data", | |
360 | rblock->name); | |
361 | return DEFER; | |
362 | } | |
363 | ||
364 | /* Get rid of leading and trailing white space, and pick off the first word of | |
365 | the result. */ | |
366 | ||
367 | while (len > 0 && isspace(buffer[len-1])) len--; | |
368 | buffer[len] = 0; | |
369 | ||
370 | DEBUG(D_route) debug_printf("command wrote: %s\n", buffer); | |
371 | ||
372 | rword = buffer; | |
373 | while (isspace(*rword)) rword++; | |
374 | rdata = rword; | |
375 | while (*rdata != 0 && !isspace(*rdata)) rdata++; | |
376 | if (*rdata != 0) *rdata++ = 0; | |
377 | ||
378 | /* The word must be a known yield name. If it is "REDIRECT", the rest of the | |
379 | line is redirection data, as for a .forward file. It may not contain filter | |
380 | data, and it may not contain anything other than addresses (no files, no pipes, | |
381 | no specials). */ | |
382 | ||
383 | if (strcmpic(rword, US"REDIRECT") == 0) | |
384 | { | |
385 | int filtertype; | |
386 | redirect_block redirect; | |
387 | address_item *generated = NULL; | |
388 | ||
389 | redirect.string = rdata; | |
390 | redirect.isfile = FALSE; | |
391 | ||
392 | rc = rda_interpret(&redirect, /* redirection data */ | |
393 | RDO_BLACKHOLE | /* forbid :blackhole: */ | |
394 | RDO_FAIL | /* forbid :fail: */ | |
395 | RDO_INCLUDE | /* forbid :include: */ | |
396 | RDO_REWRITE, /* rewrite generated addresses */ | |
397 | NULL, /* :include: directory not relevant */ | |
398 | NULL, /* sieve vacation directory not relevant */ | |
efd9a422 | 399 | NULL, /* sieve enotify mailto owner not relevant */ |
e4a89c47 PH |
400 | NULL, /* sieve useraddress not relevant */ |
401 | NULL, /* sieve subaddress not relevant */ | |
0756eb3c PH |
402 | &ugid, /* uid/gid (but not set) */ |
403 | &generated, /* where to hang the results */ | |
404 | &(addr->message), /* where to put messages */ | |
405 | NULL, /* don't skip syntax errors */ | |
406 | &filtertype, /* not used; will always be FILTER_FORWARD */ | |
407 | string_sprintf("%s router", rblock->name)); | |
408 | ||
409 | switch (rc) | |
410 | { | |
411 | /* FF_DEFER and FF_FAIL can arise only as a result of explicit commands. | |
412 | If a configured message was supplied, allow it to be included in an SMTP | |
413 | response after verifying. */ | |
414 | ||
415 | case FF_DEFER: | |
416 | if (addr->message == NULL) addr->message = US"forced defer"; | |
417 | else addr->user_message = addr->message; | |
418 | return DEFER; | |
419 | ||
420 | case FF_FAIL: | |
421 | add_generated(rblock, addr_new, addr, generated, &addr_prop); | |
422 | if (addr->message == NULL) addr->message = US"forced rejection"; | |
423 | else addr->user_message = addr->message; | |
424 | return FAIL; | |
425 | ||
426 | case FF_DELIVERED: | |
427 | break; | |
428 | ||
429 | case FF_NOTDELIVERED: /* an empty redirection list is bad */ | |
430 | addr->message = US"no addresses supplied"; | |
431 | /* Fall through */ | |
432 | ||
433 | case FF_ERROR: | |
434 | default: | |
435 | addr->basic_errno = ERRNO_BADREDIRECT; | |
436 | addr->message = string_sprintf("error in redirect data: %s", addr->message); | |
437 | return DEFER; | |
438 | } | |
439 | ||
440 | /* Handle the generated addresses, if any. */ | |
441 | ||
442 | add_generated(rblock, addr_new, addr, generated, &addr_prop); | |
443 | ||
444 | /* Put the original address onto the succeed queue so that any retry items | |
445 | that get attached to it get processed. */ | |
446 | ||
447 | addr->next = *addr_succeed; | |
448 | *addr_succeed = addr; | |
449 | ||
450 | return OK; | |
451 | } | |
452 | ||
453 | /* Handle other returns that are not ACCEPT */ | |
454 | ||
455 | if (strcmpic(rword, US"accept") != 0) | |
456 | { | |
457 | if (strcmpic(rword, US"decline") == 0) return DECLINE; | |
458 | if (strcmpic(rword, US"pass") == 0) return PASS; | |
459 | addr->message = string_copy(rdata); /* data is a message */ | |
447d236c PH |
460 | if (strcmpic(rword, US"fail") == 0) |
461 | { | |
462 | setflag(addr, af_pass_message); | |
463 | return FAIL; | |
464 | } | |
0756eb3c PH |
465 | if (strcmpic(rword, US"freeze") == 0) addr->special_action = SPECIAL_FREEZE; |
466 | else if (strcmpic(rword, US"defer") != 0) | |
467 | { | |
468 | addr->message = string_sprintf("bad command yield: %s %s", rword, rdata); | |
469 | log_write(0, LOG_PANIC, "%s router: %s", rblock->name, addr->message); | |
470 | } | |
471 | return DEFER; | |
472 | } | |
473 | ||
474 | /* The command yielded "ACCEPT". The rest of the string is a number of keyed | |
475 | fields from which we can fish out values using the "extract" expansion | |
476 | function. To use this feature, we must put the string into the $value variable, | |
477 | i.e. set lookup_value. */ | |
478 | ||
479 | lookup_value = rdata; | |
480 | s = expand_string(US"${extract{data}{$value}}"); | |
481 | if (*s != 0) addr_prop.address_data = string_copy(s); | |
482 | ||
483 | s = expand_string(US"${extract{transport}{$value}}"); | |
484 | lookup_value = NULL; | |
485 | ||
486 | /* If we found a transport name, find the actual transport */ | |
487 | ||
488 | if (*s != 0) | |
489 | { | |
490 | transport_instance *transport; | |
d7978c0f | 491 | for (transport = transports; transport; transport = transport->next) |
0756eb3c | 492 | if (Ustrcmp(transport->name, s) == 0) break; |
d7978c0f | 493 | if (!transport) |
0756eb3c PH |
494 | { |
495 | addr->message = string_sprintf("unknown transport name %s yielded by " | |
496 | "command", s); | |
497 | log_write(0, LOG_PANIC, "%s router: %s", rblock->name, addr->message); | |
498 | return DEFER; | |
499 | } | |
500 | addr->transport = transport; | |
501 | } | |
502 | ||
503 | /* No transport given; get the transport from the router configuration. It may | |
504 | be fixed or expanded, but there will be an error if it is unset, requested by | |
505 | the last argument not being NULL. */ | |
506 | ||
507 | else | |
508 | { | |
509 | if (!rf_get_transport(rblock->transport_name, &(rblock->transport), addr, | |
510 | rblock->name, US"transport")) | |
511 | return DEFER; | |
512 | addr->transport = rblock->transport; | |
513 | } | |
514 | ||
515 | /* See if a host list is given, and if so, look up the addresses. */ | |
516 | ||
517 | lookup_value = rdata; | |
518 | s = expand_string(US"${extract{hosts}{$value}}"); | |
519 | ||
520 | if (*s != 0) | |
521 | { | |
66387a73 | 522 | int lookup_type = LK_DEFAULT; |
0756eb3c PH |
523 | uschar *ss = expand_string(US"${extract{lookup}{$value}}"); |
524 | lookup_value = NULL; | |
525 | ||
526 | if (*ss != 0) | |
527 | { | |
66387a73 JH |
528 | if (Ustrcmp(ss, "byname") == 0) lookup_type = LK_BYNAME; |
529 | else if (Ustrcmp(ss, "bydns") == 0) lookup_type = LK_BYDNS; | |
0756eb3c PH |
530 | else |
531 | { | |
532 | addr->message = string_sprintf("bad lookup type \"%s\" yielded by " | |
533 | "command", ss); | |
534 | log_write(0, LOG_PANIC, "%s router: %s", rblock->name, addr->message); | |
535 | return DEFER; | |
536 | } | |
537 | } | |
538 | ||
539 | host_build_hostlist(&(addr->host_list), s, FALSE); /* pro tem no randomize */ | |
540 | ||
541 | rc = rf_lookup_hostlist(rblock, addr, rblock->ignore_target_hosts, | |
542 | lookup_type, hff_defer, addr_new); | |
543 | if (rc != OK) return rc; | |
544 | } | |
545 | lookup_value = NULL; | |
546 | ||
547 | /* Put the errors address, extra headers, and address_data into this address */ | |
548 | ||
d43cbe25 | 549 | addr->prop = addr_prop; |
0756eb3c PH |
550 | |
551 | /* Queue the address for local or remote delivery. */ | |
552 | ||
553 | return rf_queue_add(addr, addr_local, addr_remote, rblock, pw)? | |
554 | OK : DEFER; | |
555 | } | |
556 | ||
d185889f | 557 | #endif /*!MACRO_PREDEF*/ |
0756eb3c | 558 | /* End of routers/queryprogram.c */ |