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$")
37 #include "asterisk/lock.h"
38 #include "asterisk/file.h"
39 #include "asterisk/logger.h"
40 #include "asterisk/channel.h"
41 #include "asterisk/pbx.h"
42 #include "asterisk/module.h"
43 #include "asterisk/config.h"
44 #include "asterisk/say.h"
45 #include "asterisk/utils.h"
46 #include "asterisk/app.h"
48 #ifdef USE_ODBC_STORAGE
51 #include "asterisk/res_odbc.h"
53 static char odbc_database
[80] = "asterisk";
54 static char odbc_table
[80] = "voicemessages";
55 static char vmfmts
[80] = "wav";
58 static char *app
= "Directory";
60 static char *synopsis
= "Provide directory of voicemail extensions";
61 static char *descrip
=
62 " Directory(vm-context[|dial-context[|options]]): This application will present\n"
63 "the calling channel with a directory of extensions from which they can search\n"
64 "by name. The list of names and corresponding extensions is retrieved from the\n"
65 "voicemail configuration file, voicemail.conf.\n"
66 " This application will immediately exit if one of the following DTMF digits are\n"
67 "received and the extension to jump to exists:\n"
68 " 0 - Jump to the 'o' extension, if it exists.\n"
69 " * - Jump to the 'a' extension, if it exists.\n\n"
71 " vm-context - This is the context within voicemail.conf to use for the\n"
73 " dial-context - This is the dialplan context to use when looking for an\n"
74 " extension that the user has selected, or when jumping to the\n"
75 " 'o' or 'a' extension.\n\n"
77 " e - In addition to the name, also read the extension number to the\n"
78 " caller before presenting dialing options.\n"
79 " f - Allow the caller to enter the first name of a user in the directory\n"
80 " instead of using the last name.\n";
82 /* For simplicity, I'm keeping the format compatible with the voicemail config,
83 but i'm open to suggestions for isolating it */
85 #define VOICEMAIL_CONFIG "voicemail.conf"
87 /* How many digits to read in */
91 #ifdef USE_ODBC_STORAGE
92 static void retrieve_file(char *dir
)
107 obj
= fetch_odbc_obj(odbc_database
, 0);
110 ast_copy_string(fmt
, vmfmts
, sizeof(fmt
));
111 c
= strchr(fmt
, '|');
114 if (!strcasecmp(fmt
, "wav49"))
116 snprintf(full_fn
, sizeof(full_fn
), "%s.%s", dir
, fmt
);
117 res
= SQLAllocHandle(SQL_HANDLE_STMT
, obj
->con
, &stmt
);
118 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
119 ast_log(LOG_WARNING
, "SQL Alloc Handle failed!\n");
122 snprintf(sql
, sizeof(sql
), "SELECT recording FROM %s WHERE dir=? AND msgnum=-1", odbc_table
);
123 res
= SQLPrepare(stmt
, (unsigned char *)sql
, SQL_NTS
);
124 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
125 ast_log(LOG_WARNING
, "SQL Prepare failed![%s]\n", sql
);
126 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
129 SQLBindParameter(stmt
, 1, SQL_PARAM_INPUT
, SQL_C_CHAR
, SQL_CHAR
, strlen(dir
), 0, (void *)dir
, 0, NULL
);
130 res
= odbc_smart_execute(obj
, stmt
);
131 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
132 ast_log(LOG_WARNING
, "SQL Execute error!\n[%s]\n\n", sql
);
133 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
136 res
= SQLFetch(stmt
);
137 if (res
== SQL_NO_DATA
) {
138 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
140 } else if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
141 ast_log(LOG_WARNING
, "SQL Fetch error!\n[%s]\n\n", sql
);
142 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
145 fd
= open(full_fn
, O_RDWR
| O_CREAT
| O_TRUNC
, 0770);
147 ast_log(LOG_WARNING
, "Failed to write '%s': %s\n", full_fn
, strerror(errno
));
148 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
152 res
= SQLGetData(stmt
, 1, SQL_BINARY
, NULL
, 0, &colsize
);
156 lseek(fd
, fdlen
- 1, SEEK_SET
);
157 if (write(fd
, tmp
, 1) != 1) {
163 fdm
= mmap(NULL
, fdlen
, PROT_READ
| PROT_WRITE
, MAP_SHARED
, fd
, 0);
166 memset(fdm
, 0, fdlen
);
167 res
= SQLGetData(stmt
, x
+ 1, SQL_BINARY
, fdm
, fdlen
, &colsize
);
168 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
169 ast_log(LOG_WARNING
, "SQL Get Data error!\n[%s]\n\n", sql
);
170 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
174 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
177 ast_log(LOG_WARNING
, "Failed to obtain database object for '%s'!\n", odbc_database
);
186 static char *convert(const char *lastname
)
190 tmp
= ast_malloc(NUMDIGITS
+ 1);
192 while((*lastname
> 32) && lcount
< NUMDIGITS
) {
193 switch(toupper(*lastname
)) {
255 /* play name of mailbox owner.
256 * returns: -1 for bad or missing extension
257 * '1' for selected entry from directory
258 * '*' for skipped entry from directory
260 static int play_mailbox_owner(struct ast_channel
*chan
, char *context
,
261 char *dialcontext
, char *ext
, char *name
, int readext
,
268 /* Check for the VoiceMail2 greeting first */
269 snprintf(fn
, sizeof(fn
), "%s/voicemail/%s/%s/greet",
270 ast_config_AST_SPOOL_DIR
, context
, ext
);
271 #ifdef USE_ODBC_STORAGE
275 if (ast_fileexists(fn
, NULL
, chan
->language
) <= 0) {
276 /* no file, check for an old-style Voicemail greeting */
277 snprintf(fn
, sizeof(fn
), "%s/vm/%s/greet",
278 ast_config_AST_SPOOL_DIR
, ext
);
280 #ifdef USE_ODBC_STORAGE
284 if (ast_fileexists(fn
, NULL
, chan
->language
) > 0) {
285 res
= ast_stream_and_wait(chan
, fn
, chan
->language
, AST_DIGIT_ANY
);
286 ast_stopstream(chan
);
287 /* If Option 'e' was specified, also read the extension number with the name */
289 ast_stream_and_wait(chan
, "vm-extension", chan
->language
, AST_DIGIT_ANY
);
290 res
= ast_say_character_str(chan
, ext
, AST_DIGIT_ANY
, chan
->language
);
293 res
= ast_say_character_str(chan
, S_OR(name
, ext
), AST_DIGIT_ANY
, chan
->language
);
294 if (!ast_strlen_zero(name
) && readext
) {
295 ast_stream_and_wait(chan
, "vm-extension", chan
->language
, AST_DIGIT_ANY
);
296 res
= ast_say_character_str(chan
, ext
, AST_DIGIT_ANY
, chan
->language
);
299 #ifdef USE_ODBC_STORAGE
300 ast_filedelete(fn
, NULL
);
301 ast_filedelete(fn2
, NULL
);
304 for (loop
= 3 ; loop
> 0; loop
--) {
306 res
= ast_stream_and_wait(chan
, "dir-instr", chan
->language
, AST_DIGIT_ANY
);
308 res
= ast_waitfordigit(chan
, 3000);
309 ast_stopstream(chan
);
311 if (res
< 0) /* User hungup, so jump out now */
313 if (res
== '1') { /* Name selected */
315 /* We still want to set the exten though */
316 ast_copy_string(chan
->exten
, ext
, sizeof(chan
->exten
));
318 if (ast_goto_if_exists(chan
, dialcontext
, ext
, 1)) {
320 "Can't find extension '%s' in context '%s'. "
321 "Did you pass the wrong context to Directory?\n",
328 if (res
== '*') /* Skip to next match in list */
331 /* Not '1', or '*', so decrement number of tries */
338 static struct ast_config
*realtime_directory(char *context
)
340 struct ast_config
*cfg
;
341 struct ast_config
*rtdata
;
342 struct ast_category
*cat
;
343 struct ast_variable
*var
;
345 const char *fullname
;
346 const char *hidefromdir
;
349 /* Load flat file config. */
350 cfg
= ast_config_load(VOICEMAIL_CONFIG
);
353 /* Loading config failed. */
354 ast_log(LOG_WARNING
, "Loading config failed.\n");
358 /* Get realtime entries, categorized by their mailbox number
359 and present in the requested context */
360 rtdata
= ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context
, NULL
);
362 /* if there are no results, just return the entries from the config file */
366 /* Does the context exist within the config file? If not, make one */
367 cat
= ast_category_get(cfg
, context
);
369 cat
= ast_category_new(context
);
371 ast_log(LOG_WARNING
, "Out of memory\n");
372 ast_config_destroy(cfg
);
375 ast_category_append(cfg
, cat
);
379 while ( (mailbox
= ast_category_browse(rtdata
, mailbox
)) ) {
380 fullname
= ast_variable_retrieve(rtdata
, mailbox
, "fullname");
381 hidefromdir
= ast_variable_retrieve(rtdata
, mailbox
, "hidefromdir");
382 snprintf(tmp
, sizeof(tmp
), "no-password,%s,hidefromdir=%s",
383 fullname
? fullname
: "",
384 hidefromdir
? hidefromdir
: "no");
385 var
= ast_variable_new(mailbox
, tmp
);
387 ast_variable_append(cat
, var
);
389 ast_log(LOG_WARNING
, "Out of memory adding mailbox '%s'\n", mailbox
);
391 ast_config_destroy(rtdata
);
396 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
)
398 /* Read in the first three digits.. "digit" is the first digit, already read */
399 char ext
[NUMDIGITS
+ 1], *cat
;
401 struct ast_variable
*v
;
404 int lastuserchoice
= 0;
405 char *start
, *conv
, *stringp
= NULL
;
408 if (ast_strlen_zero(context
)) {
410 "Directory must be called with an argument "
411 "(context in which to interpret extensions)\n");
415 if (!ast_goto_if_exists(chan
, dialcontext
, "o", 1) ||
416 (!ast_strlen_zero(chan
->macrocontext
) &&
417 !ast_goto_if_exists(chan
, chan
->macrocontext
, "o", 1))) {
420 ast_log(LOG_WARNING
, "Can't find extension 'o' in current context. "
421 "Not Exiting the Directory!\n");
426 if (!ast_goto_if_exists(chan
, dialcontext
, "a", 1) ||
427 (!ast_strlen_zero(chan
->macrocontext
) &&
428 !ast_goto_if_exists(chan
, chan
->macrocontext
, "a", 1))) {
431 ast_log(LOG_WARNING
, "Can't find extension 'a' in current context. "
432 "Not Exiting the Directory!\n");
436 memset(ext
, 0, sizeof(ext
));
439 if (ast_readstring(chan
, ext
+ 1, NUMDIGITS
- 1, 3000, 3000, "#") < 0) res
= -1;
441 /* Search for all names which start with those digits */
442 v
= ast_variable_browse(cfg
, context
);
444 /* Find all candidate extensions */
446 /* Find a candidate extension */
447 start
= strdup(v
->value
);
448 if (start
&& !strcasestr(start
, "hidefromdir=yes")) {
450 strsep(&stringp
, ",");
451 pos
= strsep(&stringp
, ",");
453 ast_copy_string(name
, pos
, sizeof(name
));
454 /* Grab the last name */
455 if (last
&& strrchr(pos
,' '))
456 pos
= strrchr(pos
, ' ') + 1;
459 if (!strcmp(conv
, ext
)) {
475 /* We have a match -- play a greeting if they have it */
476 res
= play_mailbox_owner(chan
, context
, dialcontext
, v
->name
, name
, readext
, fromappvm
);
479 /* user pressed '1' but extension does not exist, or
485 /* user pressed '1' and extensions exists;
486 play_mailbox_owner will already have done
487 a goto() on the channel
489 lastuserchoice
= res
;
492 /* user pressed '*' to skip something found */
493 lastuserchoice
= res
;
504 /* Search users.conf for all names which start with those digits */
505 for (cat
= ast_category_browse(ucfg
, NULL
); cat
&& !res
; cat
= ast_category_browse(ucfg
, cat
)) {
506 if (!strcasecmp(cat
, "general"))
508 if (!ast_true(ast_config_option(ucfg
, cat
, "hasdirectory")))
511 /* Find all candidate extensions */
512 if ((pos
= ast_variable_retrieve(ucfg
, cat
, "fullname"))) {
513 ast_copy_string(name
, pos
, sizeof(name
));
514 /* Grab the last name */
515 if (last
&& strrchr(pos
,' '))
516 pos
= strrchr(pos
, ' ') + 1;
519 if (!strcmp(conv
, ext
)) {
522 /* We have a match -- play a greeting if they have it */
523 res
= play_mailbox_owner(chan
, context
, dialcontext
, cat
, name
, readext
, fromappvm
);
526 /* user pressed '1' but extension does not exist, or
532 /* user pressed '1' and extensions exists;
533 play_mailbox_owner will already have done
534 a goto() on the channel
536 lastuserchoice
= res
;
539 /* user pressed '*' to skip something found */
540 lastuserchoice
= res
;
555 if (lastuserchoice
!= '1') {
556 res
= ast_streamfile(chan
, found
? "dir-nomore" : "dir-nomatch", chan
->language
);
566 static int directory_exec(struct ast_channel
*chan
, void *data
)
569 struct ast_module_user
*u
;
570 struct ast_config
*cfg
, *ucfg
;
574 const char *dirintro
;
576 AST_DECLARE_APP_ARGS(args
,
577 AST_APP_ARG(vmcontext
);
578 AST_APP_ARG(dialcontext
);
579 AST_APP_ARG(options
);
582 if (ast_strlen_zero(data
)) {
583 ast_log(LOG_WARNING
, "Directory requires an argument (context[,dialcontext])\n");
587 u
= ast_module_user_add(chan
);
589 parse
= ast_strdupa(data
);
591 AST_STANDARD_APP_ARGS(args
, parse
);
594 if (strchr(args
.options
, 'f'))
596 if (strchr(args
.options
, 'e'))
598 if (strchr(args
.options
, 'v'))
602 if (ast_strlen_zero(args
.dialcontext
))
603 args
.dialcontext
= args
.vmcontext
;
605 cfg
= realtime_directory(args
.vmcontext
);
607 ast_log(LOG_ERROR
, "Unable to read the configuration data!\n");
608 ast_module_user_remove(u
);
612 ucfg
= ast_config_load("users.conf");
614 dirintro
= ast_variable_retrieve(cfg
, args
.vmcontext
, "directoryintro");
615 if (ast_strlen_zero(dirintro
))
616 dirintro
= ast_variable_retrieve(cfg
, "general", "directoryintro");
617 if (ast_strlen_zero(dirintro
))
618 dirintro
= last
? "dir-intro" : "dir-intro-fn";
620 if (chan
->_state
!= AST_STATE_UP
)
621 res
= ast_answer(chan
);
625 res
= ast_stream_and_wait(chan
, dirintro
, chan
->language
, AST_DIGIT_ANY
);
626 ast_stopstream(chan
);
628 res
= ast_waitfordigit(chan
, 5000);
630 res
= do_directory(chan
, cfg
, ucfg
, args
.vmcontext
, args
.dialcontext
, res
, last
, readext
, fromappvm
);
632 res
= ast_waitstream(chan
, AST_DIGIT_ANY
);
633 ast_stopstream(chan
);
641 ast_config_destroy(ucfg
);
642 ast_config_destroy(cfg
);
643 ast_module_user_remove(u
);
647 static int unload_module(void)
650 res
= ast_unregister_application(app
);
654 static int load_module(void)
656 #ifdef USE_ODBC_STORAGE
657 struct ast_config
*cfg
= ast_config_load(VOICEMAIL_CONFIG
);
661 if ((tmp
= ast_variable_retrieve(cfg
, "general", "odbcstorage"))) {
662 ast_copy_string(odbc_database
, tmp
, sizeof(odbc_database
));
664 if ((tmp
= ast_variable_retrieve(cfg
, "general", "odbctable"))) {
665 ast_copy_string(odbc_table
, tmp
, sizeof(odbc_table
));
667 if ((tmp
= ast_variable_retrieve(cfg
, "general", "format"))) {
668 ast_copy_string(vmfmts
, tmp
, sizeof(vmfmts
));
670 ast_config_destroy(cfg
);
672 ast_log(LOG_WARNING
, "Unable to load " VOICEMAIL_CONFIG
" - ODBC defaults will be used\n");
675 return ast_register_application(app
, directory_exec
, synopsis
, descrip
);
678 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY
, "Extension Directory");