Fix a couple of spots in the handling of device states that could lead to a
[asterisk/asterisk.git] / apps / app_directory.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2005, Digium, Inc.
5  *
6  * Mark Spencer <markster@digium.com>
7  *
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.
13  *
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.
17  */
18
19 /*! \file
20  *
21  * \brief Provide a directory of extensions
22  *
23  * \author Mark Spencer <markster@digium.com>
24  * 
25  * \ingroup applications
26  */
27  
28 #include "asterisk.h"
29
30 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
31
32 #include <string.h>
33 #include <ctype.h>
34 #include <stdlib.h>
35 #include <stdio.h>
36
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"
47
48 #ifdef ODBC_STORAGE
49 #include <errno.h>
50 #include <sys/mman.h>
51 #include "asterisk/res_odbc.h"
52
53 static char odbc_database[80] = "asterisk";
54 static char odbc_table[80] = "voicemessages";
55 static char vmfmts[80] = "wav";
56 #endif
57
58 static char *app = "Directory";
59
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"
70 "  Parameters:\n"
71 "    vm-context   - This is the context within voicemail.conf to use for the\n"
72 "                   Directory.\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"
76 "  Options:\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";
81
82 /* For simplicity, I'm keeping the format compatible with the voicemail config,
83    but i'm open to suggestions for isolating it */
84
85 #define VOICEMAIL_CONFIG "voicemail.conf"
86
87 /* How many digits to read in */
88 #define NUMDIGITS 3
89
90
91 #ifdef ODBC_STORAGE
92 static void retrieve_file(char *dir)
93 {
94         int x = 0;
95         int res;
96         int fd=-1;
97         size_t fdlen = 0;
98         void *fdm = MAP_FAILED;
99         SQLHSTMT stmt;
100         char sql[256];
101         char fmt[80]="", empty[10] = "";
102         char *c;
103         SQLLEN colsize;
104         char full_fn[256];
105         struct odbc_obj *obj;
106
107         obj = ast_odbc_request_obj(odbc_database, 1);
108         if (obj) {
109                 do {
110                         ast_copy_string(fmt, vmfmts, sizeof(fmt));
111                         c = strchr(fmt, '|');
112                         if (c)
113                                 *c = '\0';
114                         if (!strcasecmp(fmt, "wav49"))
115                                 strcpy(fmt, "WAV");
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");
120                                 break;
121                         }
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);
127                                 break;
128                         }
129                         SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, strlen(dir), 0, (void *)dir, 0, NULL);
130                         res = ast_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);
134                                 break;
135                         }
136                         res = SQLFetch(stmt);
137                         if (res == SQL_NO_DATA) {
138                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
139                                 break;
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);
143                                 break;
144                         }
145                         fd = open(full_fn, O_RDWR | O_CREAT | O_TRUNC, AST_FILE_MODE);
146                         if (fd < 0) {
147                                 ast_log(LOG_WARNING, "Failed to write '%s': %s\n", full_fn, strerror(errno));
148                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
149                                 break;
150                         }
151
152                         res = SQLGetData(stmt, 1, SQL_BINARY, empty, 0, &colsize);
153                         fdlen = colsize;
154                         if (fd > -1) {
155                                 char tmp[1]="";
156                                 lseek(fd, fdlen - 1, SEEK_SET);
157                                 if (write(fd, tmp, 1) != 1) {
158                                         close(fd);
159                                         fd = -1;
160                                         break;
161                                 }
162                                 if (fd > -1)
163                                         fdm = mmap(NULL, fdlen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
164                         }
165                         if (fdm != MAP_FAILED) {
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);
171                                         break;
172                                 }
173                         }
174                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
175                 } while (0);
176                 ast_odbc_release_obj(obj);
177         } else
178                 ast_log(LOG_WARNING, "Failed to obtain database object for '%s'!\n", odbc_database);
179         if (fdm != MAP_FAILED)
180                 munmap(fdm, fdlen);
181         if (fd > -1)
182                 close(fd);
183         return;
184 }
185 #endif
186
187 static char *convert(const char *lastname)
188 {
189         char *tmp;
190         int lcount = 0;
191         tmp = ast_malloc(NUMDIGITS + 1);
192         if (tmp) {
193                 while((*lastname > 32) && lcount < NUMDIGITS) {
194                         switch(toupper(*lastname)) {
195                         case '1':
196                                 tmp[lcount++] = '1';
197                                 break;
198                         case '2':
199                         case 'A':
200                         case 'B':
201                         case 'C':
202                                 tmp[lcount++] = '2';
203                                 break;
204                         case '3':
205                         case 'D':
206                         case 'E':
207                         case 'F':
208                                 tmp[lcount++] = '3';
209                                 break;
210                         case '4':
211                         case 'G':
212                         case 'H':
213                         case 'I':
214                                 tmp[lcount++] = '4';
215                                 break;
216                         case '5':
217                         case 'J':
218                         case 'K':
219                         case 'L':
220                                 tmp[lcount++] = '5';
221                                 break;
222                         case '6':
223                         case 'M':
224                         case 'N':
225                         case 'O':
226                                 tmp[lcount++] = '6';
227                                 break;
228                         case '7':
229                         case 'P':
230                         case 'Q':
231                         case 'R':
232                         case 'S':
233                                 tmp[lcount++] = '7';
234                                 break;
235                         case '8':
236                         case 'T':
237                         case 'U':
238                         case 'V':
239                                 tmp[lcount++] = '8';
240                                 break;
241                         case '9':
242                         case 'W':
243                         case 'X':
244                         case 'Y':
245                         case 'Z':
246                                 tmp[lcount++] = '9';
247                                 break;
248                         }
249                         lastname++;
250                 }
251                 tmp[lcount] = '\0';
252         }
253         return tmp;
254 }
255
256 /* play name of mailbox owner.
257  * returns:  -1 for bad or missing extension
258  *           '1' for selected entry from directory
259  *           '*' for skipped entry from directory
260  */
261 static int play_mailbox_owner(struct ast_channel *chan, char *context,
262                 char *dialcontext, char *ext, char *name, int readext,
263                 int fromappvm)
264 {
265         int res = 0;
266         int loop;
267         char fn[256];
268 #ifdef ODBC_STORAGE
269         char fn2[256];
270 #endif
271
272         /* Check for the VoiceMail2 greeting first */
273         snprintf(fn, sizeof(fn), "%s/voicemail/%s/%s/greet",
274                 ast_config_AST_SPOOL_DIR, context, ext);
275 #ifdef ODBC_STORAGE
276         retrieve_file(fn);
277 #endif
278
279         if (ast_fileexists(fn, NULL, chan->language) <= 0) {
280                 /* no file, check for an old-style Voicemail greeting */
281                 snprintf(fn, sizeof(fn), "%s/vm/%s/greet",
282                         ast_config_AST_SPOOL_DIR, ext);
283         }
284 #ifdef ODBC_STORAGE
285         retrieve_file(fn2);
286 #endif
287
288         if (ast_fileexists(fn, NULL, chan->language) > 0) {
289                 res = ast_stream_and_wait(chan, fn, AST_DIGIT_ANY);
290                 ast_stopstream(chan);
291                 /* If Option 'e' was specified, also read the extension number with the name */
292                 if (readext) {
293                         ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
294                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
295                 }
296         } else {
297                 res = ast_say_character_str(chan, S_OR(name, ext), AST_DIGIT_ANY, chan->language);
298                 if (!ast_strlen_zero(name) && readext) {
299                         ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
300                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
301                 }
302         }
303 #ifdef ODBC_STORAGE
304         ast_filedelete(fn, NULL);       
305         ast_filedelete(fn2, NULL);      
306 #endif
307
308         for (loop = 3 ; loop > 0; loop--) {
309                 if (!res)
310                         res = ast_stream_and_wait(chan, "dir-instr", AST_DIGIT_ANY);
311                 if (!res)
312                         res = ast_waitfordigit(chan, 3000);
313                 ast_stopstream(chan);
314         
315                 if (res < 0) /* User hungup, so jump out now */
316                         break;
317                 if (res == '1') {       /* Name selected */
318                         if (fromappvm) {
319                                 /* We still want to set the exten though */
320                                 ast_copy_string(chan->exten, ext, sizeof(chan->exten));
321                         } else {
322                                 if (ast_goto_if_exists(chan, dialcontext, ext, 1)) {
323                                         ast_log(LOG_WARNING,
324                                                 "Can't find extension '%s' in context '%s'.  "
325                                                 "Did you pass the wrong context to Directory?\n",
326                                                 ext, dialcontext);
327                                         res = -1;
328                                 }
329                         }
330                         break;
331                 }
332                 if (res == '*') /* Skip to next match in list */
333                         break;
334
335                 /* Not '1', or '*', so decrement number of tries */
336                 res = 0;
337         }
338
339         return(res);
340 }
341
342 static struct ast_config *realtime_directory(char *context)
343 {
344         struct ast_config *cfg;
345         struct ast_config *rtdata;
346         struct ast_category *cat;
347         struct ast_variable *var;
348         char *mailbox;
349         const char *fullname;
350         const char *hidefromdir;
351         char tmp[100];
352
353         /* Load flat file config. */
354         cfg = ast_config_load(VOICEMAIL_CONFIG);
355
356         if (!cfg) {
357                 /* Loading config failed. */
358                 ast_log(LOG_WARNING, "Loading config failed.\n");
359                 return NULL;
360         }
361
362         /* Get realtime entries, categorized by their mailbox number
363            and present in the requested context */
364         rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context, NULL);
365
366         /* if there are no results, just return the entries from the config file */
367         if (!rtdata)
368                 return cfg;
369
370         /* Does the context exist within the config file? If not, make one */
371         cat = ast_category_get(cfg, context);
372         if (!cat) {
373                 cat = ast_category_new(context);
374                 if (!cat) {
375                         ast_log(LOG_WARNING, "Out of memory\n");
376                         ast_config_destroy(cfg);
377                         return NULL;
378                 }
379                 ast_category_append(cfg, cat);
380         }
381
382         mailbox = NULL;
383         while ( (mailbox = ast_category_browse(rtdata, mailbox)) ) {
384                 fullname = ast_variable_retrieve(rtdata, mailbox, "fullname");
385                 hidefromdir = ast_variable_retrieve(rtdata, mailbox, "hidefromdir");
386                 snprintf(tmp, sizeof(tmp), "no-password,%s,hidefromdir=%s",
387                          fullname ? fullname : "",
388                          hidefromdir ? hidefromdir : "no");
389                 var = ast_variable_new(mailbox, tmp);
390                 if (var)
391                         ast_variable_append(cat, var);
392                 else
393                         ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
394         }
395         ast_config_destroy(rtdata);
396
397         return cfg;
398 }
399
400 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)
401 {
402         /* Read in the first three digits..  "digit" is the first digit, already read */
403         char ext[NUMDIGITS + 1], *cat;
404         char name[80] = "";
405         struct ast_variable *v;
406         int res;
407         int found=0;
408         int lastuserchoice = 0;
409         char *start, *conv, *stringp = NULL;
410         const char *pos;
411
412         if (ast_strlen_zero(context)) {
413                 ast_log(LOG_WARNING,
414                         "Directory must be called with an argument "
415                         "(context in which to interpret extensions)\n");
416                 return -1;
417         }
418         if (digit == '0') {
419                 if (!ast_goto_if_exists(chan, dialcontext, "o", 1) ||
420                     (!ast_strlen_zero(chan->macrocontext) &&
421                      !ast_goto_if_exists(chan, chan->macrocontext, "o", 1))) {
422                         return 0;
423                 } else {
424                         ast_log(LOG_WARNING, "Can't find extension 'o' in current context.  "
425                                 "Not Exiting the Directory!\n");
426                         res = 0;
427                 }
428         }       
429         if (digit == '*') {
430                 if (!ast_goto_if_exists(chan, dialcontext, "a", 1) ||
431                     (!ast_strlen_zero(chan->macrocontext) &&
432                      !ast_goto_if_exists(chan, chan->macrocontext, "a", 1))) {
433                         return 0;
434                 } else {
435                         ast_log(LOG_WARNING, "Can't find extension 'a' in current context.  "
436                                 "Not Exiting the Directory!\n");
437                         res = 0;
438                 }
439         }       
440         memset(ext, 0, sizeof(ext));
441         ext[0] = digit;
442         res = 0;
443         if (ast_readstring(chan, ext + 1, NUMDIGITS - 1, 3000, 3000, "#") < 0) res = -1;
444         if (!res) {
445                 /* Search for all names which start with those digits */
446                 v = ast_variable_browse(cfg, context);
447                 while(v && !res) {
448                         /* Find all candidate extensions */
449                         while(v) {
450                                 /* Find a candidate extension */
451                                 start = strdup(v->value);
452                                 if (start && !strcasestr(start, "hidefromdir=yes")) {
453                                         stringp=start;
454                                         strsep(&stringp, ",");
455                                         pos = strsep(&stringp, ",");
456                                         if (pos) {
457                                                 ast_copy_string(name, pos, sizeof(name));
458                                                 /* Grab the last name */
459                                                 if (last && strrchr(pos,' '))
460                                                         pos = strrchr(pos, ' ') + 1;
461                                                 conv = convert(pos);
462                                                 if (conv) {
463                                                         if (!strncmp(conv, ext, strlen(ext))) {
464                                                                 /* Match! */
465                                                                 found++;
466                                                                 free(conv);
467                                                                 free(start);
468                                                                 break;
469                                                         }
470                                                         free(conv);
471                                                 }
472                                         }
473                                         free(start);
474                                 }
475                                 v = v->next;
476                         }
477
478                         if (v) {
479                                 /* We have a match -- play a greeting if they have it */
480                                 res = play_mailbox_owner(chan, context, dialcontext, v->name, name, readext, fromappvm);
481                                 switch (res) {
482                                         case -1:
483                                                 /* user pressed '1' but extension does not exist, or
484                                                  * user hungup
485                                                  */
486                                                 lastuserchoice = 0;
487                                                 break;
488                                         case '1':
489                                                 /* user pressed '1' and extensions exists;
490                                                    play_mailbox_owner will already have done
491                                                    a goto() on the channel
492                                                  */
493                                                 lastuserchoice = res;
494                                                 break;
495                                         case '*':
496                                                 /* user pressed '*' to skip something found */
497                                                 lastuserchoice = res;
498                                                 res = 0;
499                                                 break;
500                                         default:
501                                                 break;
502                                 }
503                                 v = v->next;
504                         }
505                 }
506
507                 if (!res && ucfg) {
508                         /* Search users.conf for all names which start with those digits */
509                         for (cat = ast_category_browse(ucfg, NULL); cat && !res ; cat = ast_category_browse(ucfg, cat)) {
510                                 if (!strcasecmp(cat, "general"))
511                                         continue;
512                                 if (!ast_true(ast_config_option(ucfg, cat, "hasdirectory")))
513                                         continue;
514                                 
515                                 /* Find all candidate extensions */
516                                 if ((pos = ast_variable_retrieve(ucfg, cat, "fullname"))) {
517                                         ast_copy_string(name, pos, sizeof(name));
518                                         /* Grab the last name */
519                                         if (last && strrchr(pos,' '))
520                                                 pos = strrchr(pos, ' ') + 1;
521                                         conv = convert(pos);
522                                         if (conv) {
523                                                 if (!strcmp(conv, ext)) {
524                                                         /* Match! */
525                                                         found++;
526                                                         /* We have a match -- play a greeting if they have it */
527                                                         res = play_mailbox_owner(chan, context, dialcontext, cat, name, readext, fromappvm);
528                                                         switch (res) {
529                                                         case -1:
530                                                                 /* user pressed '1' but extension does not exist, or
531                                                                  * user hungup
532                                                                  */
533                                                                 lastuserchoice = 0;
534                                                                 break;
535                                                         case '1':
536                                                                 /* user pressed '1' and extensions exists;
537                                                                    play_mailbox_owner will already have done
538                                                                    a goto() on the channel
539                                                                  */
540                                                                 lastuserchoice = res;
541                                                                 break;
542                                                         case '*':
543                                                                 /* user pressed '*' to skip something found */
544                                                                 lastuserchoice = res;
545                                                                 res = 0;
546                                                                 break;
547                                                         default:
548                                                                 break;
549                                                         }
550                                                         free(conv);
551                                                         break;
552                                                 }
553                                                 free(conv);
554                                         }
555                                 }
556                         }
557                 }
558                         
559                 if (lastuserchoice != '1') {
560                         res = ast_streamfile(chan, found ? "dir-nomore" : "dir-nomatch", chan->language);
561                         if (!res)
562                                 res = 1;
563                         return res;
564                 }
565                 return 0;
566         }
567         return res;
568 }
569
570 static int directory_exec(struct ast_channel *chan, void *data)
571 {
572         int res = 0;
573         struct ast_module_user *u;
574         struct ast_config *cfg, *ucfg;
575         int last = 1;
576         int readext = 0;
577         int fromappvm = 0;
578         const char *dirintro;
579         char *parse;
580         AST_DECLARE_APP_ARGS(args,
581                 AST_APP_ARG(vmcontext);
582                 AST_APP_ARG(dialcontext);
583                 AST_APP_ARG(options);
584         );
585
586         if (ast_strlen_zero(data)) {
587                 ast_log(LOG_WARNING, "Directory requires an argument (context[,dialcontext])\n");
588                 return -1;
589         }
590
591         u = ast_module_user_add(chan);
592
593         parse = ast_strdupa(data);
594
595         AST_STANDARD_APP_ARGS(args, parse);
596                 
597         if (args.options) {
598                 if (strchr(args.options, 'f'))
599                         last = 0;
600                 if (strchr(args.options, 'e'))
601                         readext = 1;
602                 if (strchr(args.options, 'v'))
603                         fromappvm = 1;
604         }
605
606         if (ast_strlen_zero(args.dialcontext))  
607                 args.dialcontext = args.vmcontext;
608
609         cfg = realtime_directory(args.vmcontext);
610         if (!cfg) {
611                 ast_log(LOG_ERROR, "Unable to read the configuration data!\n");
612                 ast_module_user_remove(u);
613                 return -1;
614         }
615         
616         ucfg = ast_config_load("users.conf");
617
618         dirintro = ast_variable_retrieve(cfg, args.vmcontext, "directoryintro");
619         if (ast_strlen_zero(dirintro))
620                 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
621         if (ast_strlen_zero(dirintro))
622                 dirintro = last ? "dir-intro" : "dir-intro-fn";
623
624         if (chan->_state != AST_STATE_UP) 
625                 res = ast_answer(chan);
626
627         for (;;) {
628                 if (!res)
629                         res = ast_stream_and_wait(chan, dirintro, AST_DIGIT_ANY);
630                 ast_stopstream(chan);
631                 if (!res)
632                         res = ast_waitfordigit(chan, 5000);
633                 if (res > 0) {
634                         res = do_directory(chan, cfg, ucfg, args.vmcontext, args.dialcontext, res, last, readext, fromappvm);
635                         if (res > 0) {
636                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
637                                 ast_stopstream(chan);
638                                 if (res >= 0)
639                                         continue;
640                         }
641                 }
642                 break;
643         }
644         if (ucfg)
645                 ast_config_destroy(ucfg);
646         ast_config_destroy(cfg);
647         ast_module_user_remove(u);
648         return res;
649 }
650
651 static int unload_module(void)
652 {
653         int res;
654         res = ast_unregister_application(app);
655         return res;
656 }
657
658 static int load_module(void)
659 {
660 #ifdef ODBC_STORAGE
661         struct ast_config *cfg = ast_config_load(VOICEMAIL_CONFIG);
662         const char *tmp;
663
664         if (cfg) {
665                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbcstorage"))) {
666                         ast_copy_string(odbc_database, tmp, sizeof(odbc_database));
667                 }
668                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbctable"))) {
669                         ast_copy_string(odbc_table, tmp, sizeof(odbc_table));
670                 }
671                 if ((tmp = ast_variable_retrieve(cfg, "general", "format"))) {
672                         ast_copy_string(vmfmts, tmp, sizeof(vmfmts));
673                 }
674                 ast_config_destroy(cfg);
675         } else
676                 ast_log(LOG_WARNING, "Unable to load " VOICEMAIL_CONFIG " - ODBC defaults will be used\n");
677 #endif
678
679         return ast_register_application(app, directory_exec, synopsis, descrip);
680 }
681
682 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Extension Directory");