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"
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 */
92 struct generic_prepare_struct
{
97 static SQLHSTMT
generic_prepare(struct odbc_obj
*obj
, void *data
)
99 struct generic_prepare_struct
*gps
= data
;
103 res
= SQLAllocHandle(SQL_HANDLE_STMT
, obj
->con
, &stmt
);
104 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
105 ast_log(LOG_WARNING
, "SQL Alloc Handle failed!\n");
109 res
= SQLPrepare(stmt
, (unsigned char *)gps
->sql
, SQL_NTS
);
110 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
111 ast_log(LOG_WARNING
, "SQL Prepare failed![%s]\n", (char *)gps
->sql
);
112 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
116 if (!ast_strlen_zero(gps
->param
))
117 SQLBindParameter(stmt
, 1, SQL_PARAM_INPUT
, SQL_C_CHAR
, SQL_CHAR
, strlen(gps
->param
), 0, (void *)gps
->param
, 0, NULL
);
122 static void retrieve_file(char *dir
)
128 void *fdm
= MAP_FAILED
;
131 char fmt
[80]="", empty
[10] = "";
135 struct odbc_obj
*obj
;
136 struct generic_prepare_struct gps
= { .sql
= sql
, .param
= dir
};
138 obj
= ast_odbc_request_obj(odbc_database
, 1);
141 ast_copy_string(fmt
, vmfmts
, sizeof(fmt
));
142 c
= strchr(fmt
, '|');
145 if (!strcasecmp(fmt
, "wav49"))
147 snprintf(full_fn
, sizeof(full_fn
), "%s.%s", dir
, fmt
);
148 snprintf(sql
, sizeof(sql
), "SELECT recording FROM %s WHERE dir=? AND msgnum=-1", odbc_table
);
149 stmt
= ast_odbc_prepare_and_execute(obj
, generic_prepare
, &gps
);
152 ast_log(LOG_WARNING
, "SQL Execute error!\n[%s]\n\n", sql
);
155 res
= SQLFetch(stmt
);
156 if (res
== SQL_NO_DATA
) {
157 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
159 } else if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
160 ast_log(LOG_WARNING
, "SQL Fetch error!\n[%s]\n\n", sql
);
161 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
164 fd
= open(full_fn
, O_RDWR
| O_CREAT
| O_TRUNC
, 0770);
166 ast_log(LOG_WARNING
, "Failed to write '%s': %s\n", full_fn
, strerror(errno
));
167 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
171 res
= SQLGetData(stmt
, 1, SQL_BINARY
, empty
, 0, &colsize
);
175 lseek(fd
, fdlen
- 1, SEEK_SET
);
176 if (write(fd
, tmp
, 1) != 1) {
182 fdm
= mmap(NULL
, fdlen
, PROT_READ
| PROT_WRITE
, MAP_SHARED
, fd
, 0);
184 if (fdm
!= MAP_FAILED
) {
185 memset(fdm
, 0, fdlen
);
186 res
= SQLGetData(stmt
, x
+ 1, SQL_BINARY
, fdm
, fdlen
, &colsize
);
187 if ((res
!= SQL_SUCCESS
) && (res
!= SQL_SUCCESS_WITH_INFO
)) {
188 ast_log(LOG_WARNING
, "SQL Get Data error!\n[%s]\n\n", sql
);
189 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
193 SQLFreeHandle(SQL_HANDLE_STMT
, stmt
);
195 ast_odbc_release_obj(obj
);
197 ast_log(LOG_WARNING
, "Failed to obtain database object for '%s'!\n", odbc_database
);
198 if (fdm
!= MAP_FAILED
)
206 static char *convert(const char *lastname
)
210 tmp
= ast_malloc(NUMDIGITS
+ 1);
212 while((*lastname
> 32) && lcount
< NUMDIGITS
) {
213 switch(toupper(*lastname
)) {
275 /* play name of mailbox owner.
276 * returns: -1 for bad or missing extension
277 * '1' for selected entry from directory
278 * '*' for skipped entry from directory
280 static int play_mailbox_owner(struct ast_channel
*chan
, char *context
,
281 char *dialcontext
, char *ext
, char *name
, int readext
,
288 /* Check for the VoiceMail2 greeting first */
289 snprintf(fn
, sizeof(fn
), "%s/voicemail/%s/%s/greet",
290 ast_config_AST_SPOOL_DIR
, context
, ext
);
295 if (ast_fileexists(fn
, NULL
, chan
->language
) <= 0) {
296 /* no file, check for an old-style Voicemail greeting */
297 snprintf(fn
, sizeof(fn
), "%s/vm/%s/greet",
298 ast_config_AST_SPOOL_DIR
, ext
);
304 if (ast_fileexists(fn
, NULL
, chan
->language
) > 0) {
305 res
= ast_stream_and_wait(chan
, fn
, chan
->language
, AST_DIGIT_ANY
);
306 ast_stopstream(chan
);
307 /* If Option 'e' was specified, also read the extension number with the name */
309 ast_stream_and_wait(chan
, "vm-extension", chan
->language
, AST_DIGIT_ANY
);
310 res
= ast_say_character_str(chan
, ext
, AST_DIGIT_ANY
, chan
->language
);
313 res
= ast_say_character_str(chan
, S_OR(name
, ext
), AST_DIGIT_ANY
, chan
->language
);
314 if (!ast_strlen_zero(name
) && readext
) {
315 ast_stream_and_wait(chan
, "vm-extension", chan
->language
, AST_DIGIT_ANY
);
316 res
= ast_say_character_str(chan
, ext
, AST_DIGIT_ANY
, chan
->language
);
320 ast_filedelete(fn
, NULL
);
323 for (loop
= 3 ; loop
> 0; loop
--) {
325 res
= ast_stream_and_wait(chan
, "dir-instr", chan
->language
, AST_DIGIT_ANY
);
327 res
= ast_waitfordigit(chan
, 3000);
328 ast_stopstream(chan
);
330 if (res
< 0) /* User hungup, so jump out now */
332 if (res
== '1') { /* Name selected */
334 /* We still want to set the exten though */
335 ast_copy_string(chan
->exten
, ext
, sizeof(chan
->exten
));
337 if (ast_goto_if_exists(chan
, dialcontext
, ext
, 1)) {
339 "Can't find extension '%s' in context '%s'. "
340 "Did you pass the wrong context to Directory?\n",
347 if (res
== '*') /* Skip to next match in list */
350 /* Not '1', or '*', so decrement number of tries */
357 static struct ast_config
*realtime_directory(char *context
)
359 struct ast_config
*cfg
;
360 struct ast_config
*rtdata
;
361 struct ast_category
*cat
;
362 struct ast_variable
*var
;
364 const char *fullname
;
365 const char *hidefromdir
;
368 /* Load flat file config. */
369 cfg
= ast_config_load(VOICEMAIL_CONFIG
);
372 /* Loading config failed. */
373 ast_log(LOG_WARNING
, "Loading config failed.\n");
377 /* Get realtime entries, categorized by their mailbox number
378 and present in the requested context */
379 rtdata
= ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context
, NULL
);
381 /* if there are no results, just return the entries from the config file */
385 /* Does the context exist within the config file? If not, make one */
386 cat
= ast_category_get(cfg
, context
);
388 cat
= ast_category_new(context
);
390 ast_log(LOG_WARNING
, "Out of memory\n");
391 ast_config_destroy(cfg
);
394 ast_category_append(cfg
, cat
);
398 while ( (mailbox
= ast_category_browse(rtdata
, mailbox
)) ) {
399 fullname
= ast_variable_retrieve(rtdata
, mailbox
, "fullname");
400 hidefromdir
= ast_variable_retrieve(rtdata
, mailbox
, "hidefromdir");
401 snprintf(tmp
, sizeof(tmp
), "no-password,%s,hidefromdir=%s",
402 fullname
? fullname
: "",
403 hidefromdir
? hidefromdir
: "no");
404 var
= ast_variable_new(mailbox
, tmp
);
406 ast_variable_append(cat
, var
);
408 ast_log(LOG_WARNING
, "Out of memory adding mailbox '%s'\n", mailbox
);
410 ast_config_destroy(rtdata
);
415 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
)
417 /* Read in the first three digits.. "digit" is the first digit, already read */
418 char ext
[NUMDIGITS
+ 1], *cat
;
420 struct ast_variable
*v
;
423 int lastuserchoice
= 0;
424 char *start
, *conv
, *stringp
= NULL
;
428 if (ast_strlen_zero(context
)) {
430 "Directory must be called with an argument "
431 "(context in which to interpret extensions)\n");
435 if (!ast_goto_if_exists(chan
, dialcontext
, "o", 1) ||
436 (!ast_strlen_zero(chan
->macrocontext
) &&
437 !ast_goto_if_exists(chan
, chan
->macrocontext
, "o", 1))) {
440 ast_log(LOG_WARNING
, "Can't find extension 'o' in current context. "
441 "Not Exiting the Directory!\n");
446 if (!ast_goto_if_exists(chan
, dialcontext
, "a", 1) ||
447 (!ast_strlen_zero(chan
->macrocontext
) &&
448 !ast_goto_if_exists(chan
, chan
->macrocontext
, "a", 1))) {
451 ast_log(LOG_WARNING
, "Can't find extension 'a' in current context. "
452 "Not Exiting the Directory!\n");
456 memset(ext
, 0, sizeof(ext
));
459 if (ast_readstring(chan
, ext
+ 1, NUMDIGITS
- 1, 3000, 3000, "#") < 0) res
= -1;
461 /* Search for all names which start with those digits */
462 v
= ast_variable_browse(cfg
, context
);
464 /* Find all candidate extensions */
466 /* Find a candidate extension */
467 start
= strdup(v
->value
);
468 if (start
&& !strcasestr(start
, "hidefromdir=yes")) {
470 strsep(&stringp
, ",");
471 pos
= strsep(&stringp
, ",");
473 ast_copy_string(name
, pos
, sizeof(name
));
474 /* Grab the last name */
475 if (last
&& strrchr(pos
,' '))
476 pos
= strrchr(pos
, ' ') + 1;
479 if (!strncmp(conv
, ext
, strlen(ext
))) {
495 /* We have a match -- play a greeting if they have it */
496 res
= play_mailbox_owner(chan
, context
, dialcontext
, v
->name
, name
, readext
, fromappvm
);
499 /* user pressed '1' but extension does not exist, or
505 /* user pressed '1' and extensions exists;
506 play_mailbox_owner will already have done
507 a goto() on the channel
509 lastuserchoice
= res
;
512 /* user pressed '*' to skip something found */
513 lastuserchoice
= res
;
524 /* Search users.conf for all names which start with those digits */
525 for (cat
= ast_category_browse(ucfg
, NULL
); cat
&& !res
; cat
= ast_category_browse(ucfg
, cat
)) {
526 if (!strcasecmp(cat
, "general"))
528 if (!ast_true(ast_config_option(ucfg
, cat
, "hasdirectory")))
531 /* Find all candidate extensions */
532 if ((pos
= ast_variable_retrieve(ucfg
, cat
, "fullname"))) {
533 ast_copy_string(name
, pos
, sizeof(name
));
534 /* Grab the last name */
535 if (last
&& strrchr(pos
,' '))
536 pos
= strrchr(pos
, ' ') + 1;
539 if (!strcmp(conv
, ext
)) {
542 /* We have a match -- play a greeting if they have it */
543 res
= play_mailbox_owner(chan
, context
, dialcontext
, cat
, name
, readext
, fromappvm
);
546 /* user pressed '1' but extension does not exist, or
553 /* user pressed '1' and extensions exists;
554 play_mailbox_owner will already have done
555 a goto() on the channel
557 lastuserchoice
= res
;
561 /* user pressed '*' to skip something found */
562 lastuserchoice
= res
;
581 if (lastuserchoice
!= '1') {
582 res
= ast_streamfile(chan
, found
? "dir-nomore" : "dir-nomatch", chan
->language
);
592 static int directory_exec(struct ast_channel
*chan
, void *data
)
595 struct ast_module_user
*u
;
596 struct ast_config
*cfg
, *ucfg
;
600 const char *dirintro
;
602 AST_DECLARE_APP_ARGS(args
,
603 AST_APP_ARG(vmcontext
);
604 AST_APP_ARG(dialcontext
);
605 AST_APP_ARG(options
);
608 if (ast_strlen_zero(data
)) {
609 ast_log(LOG_WARNING
, "Directory requires an argument (context[,dialcontext])\n");
613 u
= ast_module_user_add(chan
);
615 parse
= ast_strdupa(data
);
617 AST_STANDARD_APP_ARGS(args
, parse
);
620 if (strchr(args
.options
, 'f'))
622 if (strchr(args
.options
, 'e'))
624 if (strchr(args
.options
, 'v'))
628 if (ast_strlen_zero(args
.dialcontext
))
629 args
.dialcontext
= args
.vmcontext
;
631 cfg
= realtime_directory(args
.vmcontext
);
633 ast_log(LOG_ERROR
, "Unable to read the configuration data!\n");
634 ast_module_user_remove(u
);
638 ucfg
= ast_config_load("users.conf");
640 dirintro
= ast_variable_retrieve(cfg
, args
.vmcontext
, "directoryintro");
641 if (ast_strlen_zero(dirintro
))
642 dirintro
= ast_variable_retrieve(cfg
, "general", "directoryintro");
643 if (ast_strlen_zero(dirintro
))
644 dirintro
= last
? "dir-intro" : "dir-intro-fn";
646 if (chan
->_state
!= AST_STATE_UP
)
647 res
= ast_answer(chan
);
651 res
= ast_stream_and_wait(chan
, dirintro
, chan
->language
, AST_DIGIT_ANY
);
652 ast_stopstream(chan
);
654 res
= ast_waitfordigit(chan
, 5000);
656 res
= do_directory(chan
, cfg
, ucfg
, args
.vmcontext
, args
.dialcontext
, res
, last
, readext
, fromappvm
);
658 res
= ast_waitstream(chan
, AST_DIGIT_ANY
);
659 ast_stopstream(chan
);
667 ast_config_destroy(ucfg
);
668 ast_config_destroy(cfg
);
669 ast_module_user_remove(u
);
673 static int unload_module(void)
676 res
= ast_unregister_application(app
);
680 static int load_module(void)
683 struct ast_config
*cfg
= ast_config_load(VOICEMAIL_CONFIG
);
687 if ((tmp
= ast_variable_retrieve(cfg
, "general", "odbcstorage"))) {
688 ast_copy_string(odbc_database
, tmp
, sizeof(odbc_database
));
690 if ((tmp
= ast_variable_retrieve(cfg
, "general", "odbctable"))) {
691 ast_copy_string(odbc_table
, tmp
, sizeof(odbc_table
));
693 if ((tmp
= ast_variable_retrieve(cfg
, "general", "format"))) {
694 ast_copy_string(vmfmts
, tmp
, sizeof(vmfmts
));
696 ast_config_destroy(cfg
);
698 ast_log(LOG_WARNING
, "Unable to load " VOICEMAIL_CONFIG
" - ODBC defaults will be used\n");
701 return ast_register_application(app
, directory_exec
, synopsis
, descrip
);
704 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY
, "Extension Directory");