2 * Asterisk -- An open source telephony toolkit.
4 * Copyright (C) 1999 - 2005, Digium, Inc.
6 * Mark Spencer <markster@digium.com>
8 * See http://www.asterisk.org for more information about
9 * the Asterisk project. Please do not directly contact
10 * any of the maintainers of this project for assistance;
11 * the project provides a web site, mailing lists and IRC
12 * channels for your use.
14 * This program is free software, distributed under the terms of
15 * the GNU General Public License Version 2. See the LICENSE file
16 * at the top of the source tree.
21 * \brief Provide a directory of extensions
23 * \author Mark Spencer <markster@digium.com>
25 * \ingroup applications
30 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
34 #include "asterisk/paths.h" /* use ast_config_AST_SPOOL_DIR */
35 #include "asterisk/file.h"
36 #include "asterisk/pbx.h"
37 #include "asterisk/module.h"
38 #include "asterisk/say.h"
39 #include "asterisk/app.h"
43 #include "asterisk/res_odbc.h"
45 static char odbc_database[80] = "asterisk";
46 static char odbc_table[80] = "voicemessages";
47 static char vmfmts[80] = "wav";
50 static char *app = "Directory";
52 static char *synopsis = "Provide directory of voicemail extensions";
53 static char *descrip =
54 " Directory(vm-context[,dial-context[,options]]): This application will present\n"
55 "the calling channel with a directory of extensions from which they can search\n"
56 "by name. The list of names and corresponding extensions is retrieved from the\n"
57 "voicemail configuration file, voicemail.conf.\n"
58 " This application will immediately exit if one of the following DTMF digits are\n"
59 "received and the extension to jump to exists:\n"
60 " 0 - Jump to the 'o' extension, if it exists.\n"
61 " * - Jump to the 'a' extension, if it exists.\n\n"
63 " vm-context - This is the context within voicemail.conf to use for the\n"
65 " dial-context - This is the dialplan context to use when looking for an\n"
66 " extension that the user has selected, or when jumping to the\n"
67 " 'o' or 'a' extension.\n\n"
69 " e - In addition to the name, also read the extension number to the\n"
70 " caller before presenting dialing options.\n"
71 " f - Allow the caller to enter the first name of a user in the directory\n"
72 " instead of using the last name.\n";
74 /* For simplicity, I'm keeping the format compatible with the voicemail config,
75 but i'm open to suggestions for isolating it */
77 #define VOICEMAIL_CONFIG "voicemail.conf"
79 /* How many digits to read in */
84 struct generic_prepare_struct {
89 static SQLHSTMT generic_prepare(struct odbc_obj *obj, void *data)
91 struct generic_prepare_struct *gps = data;
95 res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt);
96 if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
97 ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n");
101 res = SQLPrepare(stmt, (unsigned char *)gps->sql, SQL_NTS);
102 if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
103 ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", (char *)gps->sql);
104 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
108 if (!ast_strlen_zero(gps->param))
109 SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, strlen(gps->param), 0, (void *)gps->param, 0, NULL);
114 static void retrieve_file(char *dir)
120 void *fdm = MAP_FAILED;
123 char fmt[80]="", empty[10] = "";
127 struct odbc_obj *obj;
128 struct generic_prepare_struct gps = { .sql = sql, .param = dir };
130 obj = ast_odbc_request_obj(odbc_database, 1);
133 ast_copy_string(fmt, vmfmts, sizeof(fmt));
134 c = strchr(fmt, '|');
137 if (!strcasecmp(fmt, "wav49"))
139 snprintf(full_fn, sizeof(full_fn), "%s.%s", dir, fmt);
140 snprintf(sql, sizeof(sql), "SELECT recording FROM %s WHERE dir=? AND msgnum=-1", odbc_table);
141 stmt = ast_odbc_prepare_and_execute(obj, generic_prepare, &gps);
144 ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", sql);
147 res = SQLFetch(stmt);
148 if (res == SQL_NO_DATA) {
149 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
151 } else if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
152 ast_log(LOG_WARNING, "SQL Fetch error!\n[%s]\n\n", sql);
153 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
156 fd = open(full_fn, O_RDWR | O_CREAT | O_TRUNC, AST_FILE_MODE);
158 ast_log(LOG_WARNING, "Failed to write '%s': %s\n", full_fn, strerror(errno));
159 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
163 res = SQLGetData(stmt, 1, SQL_BINARY, empty, 0, &colsize);
167 lseek(fd, fdlen - 1, SEEK_SET);
168 if (write(fd, tmp, 1) != 1) {
174 fdm = mmap(NULL, fdlen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
176 if (fdm != MAP_FAILED) {
177 memset(fdm, 0, fdlen);
178 res = SQLGetData(stmt, x + 1, SQL_BINARY, fdm, fdlen, &colsize);
179 if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
180 ast_log(LOG_WARNING, "SQL Get Data error!\n[%s]\n\n", sql);
181 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
185 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
187 ast_odbc_release_obj(obj);
189 ast_log(LOG_WARNING, "Failed to obtain database object for '%s'!\n", odbc_database);
190 if (fdm != MAP_FAILED)
198 static char *convert(const char *lastname)
202 tmp = ast_malloc(NUMDIGITS + 1);
204 while((*lastname > 32) && lcount < NUMDIGITS) {
205 switch(toupper(*lastname)) {
267 /* play name of mailbox owner.
268 * returns: -1 for bad or missing extension
269 * '1' for selected entry from directory
270 * '*' for skipped entry from directory
272 static int play_mailbox_owner(struct ast_channel *chan, const char *context,
273 const char *dialcontext, const char *ext, const char *name, int readext,
280 /* Check for the VoiceMail2 greeting first */
281 snprintf(fn, sizeof(fn), "%s/voicemail/%s/%s/greet",
282 ast_config_AST_SPOOL_DIR, context, ext);
287 if (ast_fileexists(fn, NULL, chan->language) <= 0) {
288 /* no file, check for an old-style Voicemail greeting */
289 snprintf(fn, sizeof(fn), "%s/vm/%s/greet",
290 ast_config_AST_SPOOL_DIR, ext);
296 if (ast_fileexists(fn, NULL, chan->language) > 0) {
297 res = ast_stream_and_wait(chan, fn, AST_DIGIT_ANY);
298 ast_stopstream(chan);
299 /* If Option 'e' was specified, also read the extension number with the name */
301 ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
302 res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
305 res = ast_say_character_str(chan, S_OR(name, ext), AST_DIGIT_ANY, chan->language);
306 if (!ast_strlen_zero(name) && readext) {
307 ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
308 res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
312 ast_filedelete(fn, NULL);
315 for (loop = 3 ; loop > 0; loop--) {
317 res = ast_stream_and_wait(chan, "dir-instr", AST_DIGIT_ANY);
319 res = ast_waitfordigit(chan, 3000);
320 ast_stopstream(chan);
322 if (res < 0) /* User hungup, so jump out now */
324 if (res == '1') { /* Name selected */
326 /* We still want to set the exten though */
327 ast_copy_string(chan->exten, ext, sizeof(chan->exten));
329 if (ast_goto_if_exists(chan, dialcontext, ext, 1)) {
331 "Can't find extension '%s' in context '%s'. "
332 "Did you pass the wrong context to Directory?\n",
339 if (res == '*') /* Skip to next match in list */
342 /* Not '1', or '*', so decrement number of tries */
349 static struct ast_config *realtime_directory(char *context)
351 struct ast_config *cfg;
352 struct ast_config *rtdata;
353 struct ast_category *cat;
354 struct ast_variable *var;
356 const char *fullname;
357 const char *hidefromdir;
359 struct ast_flags config_flags = { 0 };
361 /* Load flat file config. */
362 cfg = ast_config_load(VOICEMAIL_CONFIG, config_flags);
365 /* Loading config failed. */
366 ast_log(LOG_WARNING, "Loading config failed.\n");
370 /* Get realtime entries, categorized by their mailbox number
371 and present in the requested context */
372 rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context, NULL);
374 /* if there are no results, just return the entries from the config file */
378 /* Does the context exist within the config file? If not, make one */
379 cat = ast_category_get(cfg, context);
381 cat = ast_category_new(context, "", 99999);
383 ast_log(LOG_WARNING, "Out of memory\n");
384 ast_config_destroy(cfg);
387 ast_category_append(cfg, cat);
391 while ( (mailbox = ast_category_browse(rtdata, mailbox)) ) {
392 fullname = ast_variable_retrieve(rtdata, mailbox, "fullname");
393 hidefromdir = ast_variable_retrieve(rtdata, mailbox, "hidefromdir");
394 snprintf(tmp, sizeof(tmp), "no-password,%s,hidefromdir=%s",
395 fullname ? fullname : "",
396 hidefromdir ? hidefromdir : "no");
397 var = ast_variable_new(mailbox, tmp, "");
399 ast_variable_append(cat, var);
401 ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
403 ast_config_destroy(rtdata);
408 static int do_directory(struct ast_channel *chan, struct ast_config *cfg, struct ast_config *ucfg, char *context, char *dialcontext, char digit, int last, int readext, int fromappvm)
410 /* Read in the first three digits.. "digit" is the first digit, already read */
411 char ext[NUMDIGITS + 1], *cat;
413 struct ast_variable *v;
416 int lastuserchoice = 0;
417 char *start, *conv, *stringp = NULL;
421 if (ast_strlen_zero(context)) {
423 "Directory must be called with an argument "
424 "(context in which to interpret extensions)\n");
428 if (!ast_goto_if_exists(chan, dialcontext, "o", 1) ||
429 (!ast_strlen_zero(chan->macrocontext) &&
430 !ast_goto_if_exists(chan, chan->macrocontext, "o", 1))) {
433 ast_log(LOG_WARNING, "Can't find extension 'o' in current context. "
434 "Not Exiting the Directory!\n");
439 if (!ast_goto_if_exists(chan, dialcontext, "a", 1) ||
440 (!ast_strlen_zero(chan->macrocontext) &&
441 !ast_goto_if_exists(chan, chan->macrocontext, "a", 1))) {
444 ast_log(LOG_WARNING, "Can't find extension 'a' in current context. "
445 "Not Exiting the Directory!\n");
449 memset(ext, 0, sizeof(ext));
452 if (ast_readstring(chan, ext + 1, NUMDIGITS - 1, 3000, 3000, "#") < 0) res = -1;
454 /* Search for all names which start with those digits */
455 v = ast_variable_browse(cfg, context);
457 /* Find all candidate extensions */
459 /* Find a candidate extension */
460 start = ast_strdup(v->value);
461 if (start && !strcasestr(start, "hidefromdir=yes")) {
463 strsep(&stringp, ",");
464 pos = strsep(&stringp, ",");
466 ast_copy_string(name, pos, sizeof(name));
467 /* Grab the last name */
468 if (last && strrchr(pos,' '))
469 pos = strrchr(pos, ' ') + 1;
472 if (!strncmp(conv, ext, strlen(ext))) {
488 /* We have a match -- play a greeting if they have it */
489 res = play_mailbox_owner(chan, context, dialcontext, v->name, name, readext, fromappvm);
492 /* user pressed '1' but extension does not exist, or
498 /* user pressed '1' and extensions exists;
499 play_mailbox_owner will already have done
500 a goto() on the channel
502 lastuserchoice = res;
505 /* user pressed '*' to skip something found */
506 lastuserchoice = res;
517 /* Search users.conf for all names which start with those digits */
518 for (cat = ast_category_browse(ucfg, NULL); cat && !res ; cat = ast_category_browse(ucfg, cat)) {
519 if (!strcasecmp(cat, "general"))
521 if (!ast_true(ast_config_option(ucfg, cat, "hasdirectory")))
524 /* Find all candidate extensions */
525 if ((pos = ast_variable_retrieve(ucfg, cat, "fullname"))) {
526 ast_copy_string(name, pos, sizeof(name));
527 /* Grab the last name */
528 if (last && strrchr(pos,' '))
529 pos = strrchr(pos, ' ') + 1;
532 if (!strcmp(conv, ext)) {
535 /* We have a match -- play a greeting if they have it */
536 res = play_mailbox_owner(chan, context, dialcontext, cat, name, readext, fromappvm);
539 /* user pressed '1' but extension does not exist, or
546 /* user pressed '1' and extensions exists;
547 play_mailbox_owner will already have done
548 a goto() on the channel
550 lastuserchoice = res;
554 /* user pressed '*' to skip something found */
555 lastuserchoice = res;
573 if (lastuserchoice != '1') {
574 res = ast_streamfile(chan, found ? "dir-nomore" : "dir-nomatch", chan->language);
584 static int directory_exec(struct ast_channel *chan, void *data)
587 struct ast_config *cfg, *ucfg;
591 const char *dirintro;
593 struct ast_flags config_flags = { 0 };
594 AST_DECLARE_APP_ARGS(args,
595 AST_APP_ARG(vmcontext);
596 AST_APP_ARG(dialcontext);
597 AST_APP_ARG(options);
600 if (ast_strlen_zero(data)) {
601 ast_log(LOG_WARNING, "Directory requires an argument (context[,dialcontext])\n");
605 parse = ast_strdupa(data);
607 AST_STANDARD_APP_ARGS(args, parse);
610 if (strchr(args.options, 'f'))
612 if (strchr(args.options, 'e'))
614 if (strchr(args.options, 'v'))
618 if (ast_strlen_zero(args.dialcontext))
619 args.dialcontext = args.vmcontext;
621 cfg = realtime_directory(args.vmcontext);
623 ast_log(LOG_ERROR, "Unable to read the configuration data!\n");
627 ucfg = ast_config_load("users.conf", config_flags);
629 dirintro = ast_variable_retrieve(cfg, args.vmcontext, "directoryintro");
630 if (ast_strlen_zero(dirintro))
631 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
632 if (ast_strlen_zero(dirintro))
633 dirintro = last ? "dir-intro" : "dir-intro-fn";
635 if (chan->_state != AST_STATE_UP)
636 res = ast_answer(chan);
640 res = ast_stream_and_wait(chan, dirintro, AST_DIGIT_ANY);
641 ast_stopstream(chan);
643 res = ast_waitfordigit(chan, 5000);
645 res = do_directory(chan, cfg, ucfg, args.vmcontext, args.dialcontext, res, last, readext, fromappvm);
647 res = ast_waitstream(chan, AST_DIGIT_ANY);
648 ast_stopstream(chan);
656 ast_config_destroy(ucfg);
657 ast_config_destroy(cfg);
661 static int unload_module(void)
664 res = ast_unregister_application(app);
668 static int load_module(void)
671 struct ast_flags config_flags = { 0 };
672 struct ast_config *cfg = ast_config_load(VOICEMAIL_CONFIG, config_flags);
676 if ((tmp = ast_variable_retrieve(cfg, "general", "odbcstorage"))) {
677 ast_copy_string(odbc_database, tmp, sizeof(odbc_database));
679 if ((tmp = ast_variable_retrieve(cfg, "general", "odbctable"))) {
680 ast_copy_string(odbc_table, tmp, sizeof(odbc_table));
682 if ((tmp = ast_variable_retrieve(cfg, "general", "format"))) {
683 ast_copy_string(vmfmts, tmp, sizeof(vmfmts));
685 ast_config_destroy(cfg);
687 ast_log(LOG_WARNING, "Unable to load " VOICEMAIL_CONFIG " - ODBC defaults will be used\n");
690 return ast_register_application(app, directory_exec, synopsis, descrip);
693 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Extension Directory");