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