Mostly cleanup of documentation to substitute the pipe with the comma, but a few...
[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
269         /* Check for the VoiceMail2 greeting first */
270         snprintf(fn, sizeof(fn), "%s/voicemail/%s/%s/greet",
271                 ast_config_AST_SPOOL_DIR, context, ext);
272 #ifdef ODBC_STORAGE
273         retrieve_file(fn);
274 #endif
275
276         if (ast_fileexists(fn, NULL, chan->language) <= 0) {
277                 /* no file, check for an old-style Voicemail greeting */
278                 snprintf(fn, sizeof(fn), "%s/vm/%s/greet",
279                         ast_config_AST_SPOOL_DIR, ext);
280         }
281 #ifdef ODBC_STORAGE
282         retrieve_file(fn);
283 #endif
284
285         if (ast_fileexists(fn, NULL, chan->language) > 0) {
286                 res = ast_stream_and_wait(chan, fn, AST_DIGIT_ANY);
287                 ast_stopstream(chan);
288                 /* If Option 'e' was specified, also read the extension number with the name */
289                 if (readext) {
290                         ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
291                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
292                 }
293         } else {
294                 res = ast_say_character_str(chan, S_OR(name, ext), AST_DIGIT_ANY, chan->language);
295                 if (!ast_strlen_zero(name) && readext) {
296                         ast_stream_and_wait(chan, "vm-extension", AST_DIGIT_ANY);
297                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
298                 }
299         }
300 #ifdef ODBC_STORAGE
301         ast_filedelete(fn, NULL);       
302 #endif
303
304         for (loop = 3 ; loop > 0; loop--) {
305                 if (!res)
306                         res = ast_stream_and_wait(chan, "dir-instr", AST_DIGIT_ANY);
307                 if (!res)
308                         res = ast_waitfordigit(chan, 3000);
309                 ast_stopstream(chan);
310         
311                 if (res < 0) /* User hungup, so jump out now */
312                         break;
313                 if (res == '1') {       /* Name selected */
314                         if (fromappvm) {
315                                 /* We still want to set the exten though */
316                                 ast_copy_string(chan->exten, ext, sizeof(chan->exten));
317                         } else {
318                                 if (ast_goto_if_exists(chan, dialcontext, ext, 1)) {
319                                         ast_log(LOG_WARNING,
320                                                 "Can't find extension '%s' in context '%s'.  "
321                                                 "Did you pass the wrong context to Directory?\n",
322                                                 ext, dialcontext);
323                                         res = -1;
324                                 }
325                         }
326                         break;
327                 }
328                 if (res == '*') /* Skip to next match in list */
329                         break;
330
331                 /* Not '1', or '*', so decrement number of tries */
332                 res = 0;
333         }
334
335         return(res);
336 }
337
338 static struct ast_config *realtime_directory(char *context)
339 {
340         struct ast_config *cfg;
341         struct ast_config *rtdata;
342         struct ast_category *cat;
343         struct ast_variable *var;
344         char *mailbox;
345         const char *fullname;
346         const char *hidefromdir;
347         char tmp[100];
348
349         /* Load flat file config. */
350         cfg = ast_config_load(VOICEMAIL_CONFIG);
351
352         if (!cfg) {
353                 /* Loading config failed. */
354                 ast_log(LOG_WARNING, "Loading config failed.\n");
355                 return NULL;
356         }
357
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);
361
362         /* if there are no results, just return the entries from the config file */
363         if (!rtdata)
364                 return cfg;
365
366         /* Does the context exist within the config file? If not, make one */
367         cat = ast_category_get(cfg, context);
368         if (!cat) {
369                 cat = ast_category_new(context);
370                 if (!cat) {
371                         ast_log(LOG_WARNING, "Out of memory\n");
372                         ast_config_destroy(cfg);
373                         return NULL;
374                 }
375                 ast_category_append(cfg, cat);
376         }
377
378         mailbox = NULL;
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);
386                 if (var)
387                         ast_variable_append(cat, var);
388                 else
389                         ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
390         }
391         ast_config_destroy(rtdata);
392
393         return cfg;
394 }
395
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)
397 {
398         /* Read in the first three digits..  "digit" is the first digit, already read */
399         char ext[NUMDIGITS + 1], *cat;
400         char name[80] = "";
401         struct ast_variable *v;
402         int res;
403         int found=0;
404         int lastuserchoice = 0;
405         char *start, *conv, *stringp = NULL;
406         const char *pos;
407         int breakout = 0;
408
409         if (ast_strlen_zero(context)) {
410                 ast_log(LOG_WARNING,
411                         "Directory must be called with an argument "
412                         "(context in which to interpret extensions)\n");
413                 return -1;
414         }
415         if (digit == '0') {
416                 if (!ast_goto_if_exists(chan, dialcontext, "o", 1) ||
417                     (!ast_strlen_zero(chan->macrocontext) &&
418                      !ast_goto_if_exists(chan, chan->macrocontext, "o", 1))) {
419                         return 0;
420                 } else {
421                         ast_log(LOG_WARNING, "Can't find extension 'o' in current context.  "
422                                 "Not Exiting the Directory!\n");
423                         res = 0;
424                 }
425         }       
426         if (digit == '*') {
427                 if (!ast_goto_if_exists(chan, dialcontext, "a", 1) ||
428                     (!ast_strlen_zero(chan->macrocontext) &&
429                      !ast_goto_if_exists(chan, chan->macrocontext, "a", 1))) {
430                         return 0;
431                 } else {
432                         ast_log(LOG_WARNING, "Can't find extension 'a' in current context.  "
433                                 "Not Exiting the Directory!\n");
434                         res = 0;
435                 }
436         }       
437         memset(ext, 0, sizeof(ext));
438         ext[0] = digit;
439         res = 0;
440         if (ast_readstring(chan, ext + 1, NUMDIGITS - 1, 3000, 3000, "#") < 0) res = -1;
441         if (!res) {
442                 /* Search for all names which start with those digits */
443                 v = ast_variable_browse(cfg, context);
444                 while(v && !res) {
445                         /* Find all candidate extensions */
446                         while(v) {
447                                 /* Find a candidate extension */
448                                 start = ast_strdup(v->value);
449                                 if (start && !strcasestr(start, "hidefromdir=yes")) {
450                                         stringp=start;
451                                         strsep(&stringp, ",");
452                                         pos = strsep(&stringp, ",");
453                                         if (pos) {
454                                                 ast_copy_string(name, pos, sizeof(name));
455                                                 /* Grab the last name */
456                                                 if (last && strrchr(pos,' '))
457                                                         pos = strrchr(pos, ' ') + 1;
458                                                 conv = convert(pos);
459                                                 if (conv) {
460                                                         if (!strncmp(conv, ext, strlen(ext))) {
461                                                                 /* Match! */
462                                                                 found++;
463                                                                 ast_free(conv);
464                                                                 ast_free(start);
465                                                                 break;
466                                                         }
467                                                         ast_free(conv);
468                                                 }
469                                         }
470                                         ast_free(start);
471                                 }
472                                 v = v->next;
473                         }
474
475                         if (v) {
476                                 /* We have a match -- play a greeting if they have it */
477                                 res = play_mailbox_owner(chan, context, dialcontext, v->name, name, readext, fromappvm);
478                                 switch (res) {
479                                         case -1:
480                                                 /* user pressed '1' but extension does not exist, or
481                                                  * user hungup
482                                                  */
483                                                 lastuserchoice = 0;
484                                                 break;
485                                         case '1':
486                                                 /* user pressed '1' and extensions exists;
487                                                    play_mailbox_owner will already have done
488                                                    a goto() on the channel
489                                                  */
490                                                 lastuserchoice = res;
491                                                 break;
492                                         case '*':
493                                                 /* user pressed '*' to skip something found */
494                                                 lastuserchoice = res;
495                                                 res = 0;
496                                                 break;
497                                         default:
498                                                 break;
499                                 }
500                                 v = v->next;
501                         }
502                 }
503
504                 if (!res && ucfg) {
505                         /* Search users.conf for all names which start with those digits */
506                         for (cat = ast_category_browse(ucfg, NULL); cat && !res ; cat = ast_category_browse(ucfg, cat)) {
507                                 if (!strcasecmp(cat, "general"))
508                                         continue;
509                                 if (!ast_true(ast_config_option(ucfg, cat, "hasdirectory")))
510                                         continue;
511                                 
512                                 /* Find all candidate extensions */
513                                 if ((pos = ast_variable_retrieve(ucfg, cat, "fullname"))) {
514                                         ast_copy_string(name, pos, sizeof(name));
515                                         /* Grab the last name */
516                                         if (last && strrchr(pos,' '))
517                                                 pos = strrchr(pos, ' ') + 1;
518                                         conv = convert(pos);
519                                         if (conv) {
520                                                 if (!strcmp(conv, ext)) {
521                                                         /* Match! */
522                                                         found++;
523                                                         /* We have a match -- play a greeting if they have it */
524                                                         res = play_mailbox_owner(chan, context, dialcontext, cat, name, readext, fromappvm);
525                                                         switch (res) {
526                                                         case -1:
527                                                                 /* user pressed '1' but extension does not exist, or
528                                                                  * user hungup
529                                                                  */
530                                                                 lastuserchoice = 0;
531                                                                 breakout = 1;
532                                                                 break;
533                                                         case '1':
534                                                                 /* user pressed '1' and extensions exists;
535                                                                    play_mailbox_owner will already have done
536                                                                    a goto() on the channel
537                                                                  */
538                                                                 lastuserchoice = res;
539                                                                 breakout = 1;
540                                                                 break;
541                                                         case '*':
542                                                                 /* user pressed '*' to skip something found */
543                                                                 lastuserchoice = res;
544                                                                 breakout = 0;
545                                                                 res = 0;
546                                                                 break;
547                                                         default:
548                                                                 breakout = 1;
549                                                                 break;
550                                                         }
551                                                         ast_free(conv);
552                                                         if (breakout)
553                                                                 break;
554                                                 } else
555                                                         ast_free(conv);
556                                         }
557                                 }
558                         }
559                 }
560                         
561                 if (lastuserchoice != '1') {
562                         res = ast_streamfile(chan, found ? "dir-nomore" : "dir-nomatch", chan->language);
563                         if (!res)
564                                 res = 1;
565                         return res;
566                 }
567                 return 0;
568         }
569         return res;
570 }
571
572 static int directory_exec(struct ast_channel *chan, void *data)
573 {
574         int res = 0;
575         struct ast_config *cfg, *ucfg;
576         int last = 1;
577         int readext = 0;
578         int fromappvm = 0;
579         const char *dirintro;
580         char *parse;
581         AST_DECLARE_APP_ARGS(args,
582                 AST_APP_ARG(vmcontext);
583                 AST_APP_ARG(dialcontext);
584                 AST_APP_ARG(options);
585         );
586
587         if (ast_strlen_zero(data)) {
588                 ast_log(LOG_WARNING, "Directory requires an argument (context[,dialcontext])\n");
589                 return -1;
590         }
591
592         parse = ast_strdupa(data);
593
594         AST_STANDARD_APP_ARGS(args, parse);
595                 
596         if (args.options) {
597                 if (strchr(args.options, 'f'))
598                         last = 0;
599                 if (strchr(args.options, 'e'))
600                         readext = 1;
601                 if (strchr(args.options, 'v'))
602                         fromappvm = 1;
603         }
604
605         if (ast_strlen_zero(args.dialcontext))  
606                 args.dialcontext = args.vmcontext;
607
608         cfg = realtime_directory(args.vmcontext);
609         if (!cfg) {
610                 ast_log(LOG_ERROR, "Unable to read the configuration data!\n");
611                 return -1;
612         }
613         
614         ucfg = ast_config_load("users.conf");
615
616         dirintro = ast_variable_retrieve(cfg, args.vmcontext, "directoryintro");
617         if (ast_strlen_zero(dirintro))
618                 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
619         if (ast_strlen_zero(dirintro))
620                 dirintro = last ? "dir-intro" : "dir-intro-fn";
621
622         if (chan->_state != AST_STATE_UP) 
623                 res = ast_answer(chan);
624
625         for (;;) {
626                 if (!res)
627                         res = ast_stream_and_wait(chan, dirintro, AST_DIGIT_ANY);
628                 ast_stopstream(chan);
629                 if (!res)
630                         res = ast_waitfordigit(chan, 5000);
631                 if (res > 0) {
632                         res = do_directory(chan, cfg, ucfg, args.vmcontext, args.dialcontext, res, last, readext, fromappvm);
633                         if (res > 0) {
634                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
635                                 ast_stopstream(chan);
636                                 if (res >= 0)
637                                         continue;
638                         }
639                 }
640                 break;
641         }
642         if (ucfg)
643                 ast_config_destroy(ucfg);
644         ast_config_destroy(cfg);
645         return res;
646 }
647
648 static int unload_module(void)
649 {
650         int res;
651         res = ast_unregister_application(app);
652         return res;
653 }
654
655 static int load_module(void)
656 {
657 #ifdef ODBC_STORAGE
658         struct ast_config *cfg = ast_config_load(VOICEMAIL_CONFIG);
659         const char *tmp;
660
661         if (cfg) {
662                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbcstorage"))) {
663                         ast_copy_string(odbc_database, tmp, sizeof(odbc_database));
664                 }
665                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbctable"))) {
666                         ast_copy_string(odbc_table, tmp, sizeof(odbc_table));
667                 }
668                 if ((tmp = ast_variable_retrieve(cfg, "general", "format"))) {
669                         ast_copy_string(vmfmts, tmp, sizeof(vmfmts));
670                 }
671                 ast_config_destroy(cfg);
672         } else
673                 ast_log(LOG_WARNING, "Unable to load " VOICEMAIL_CONFIG " - ODBC defaults will be used\n");
674 #endif
675
676         return ast_register_application(app, directory_exec, synopsis, descrip);
677 }
678
679 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Extension Directory");