Fix a bug with app_voicemail when trying to use app_directory to leave messages
[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 USE_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 LOCAL_USER_DECL;
91
92 #ifdef USE_ODBC_STORAGE
93 static void retrieve_file(char *dir)
94 {
95         int x = 0;
96         int res;
97         int fd=-1;
98         size_t fdlen = 0;
99         void *fdm=NULL;
100         SQLHSTMT stmt;
101         char sql[256];
102         char fmt[80]="";
103         char *c;
104         SQLLEN colsize;
105         char full_fn[256];
106
107         odbc_obj *obj;
108         obj = fetch_odbc_obj(odbc_database, 0);
109         if (obj) {
110                 do {
111                         ast_copy_string(fmt, vmfmts, sizeof(fmt));
112                         c = strchr(fmt, '|');
113                         if (c)
114                                 *c = '\0';
115                         if (!strcasecmp(fmt, "wav49"))
116                                 strcpy(fmt, "WAV");
117                         snprintf(full_fn, sizeof(full_fn), "%s.%s", dir, fmt);
118                         res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt);
119                         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
120                                 ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n");
121                                 break;
122                         }
123                         snprintf(sql, sizeof(sql), "SELECT recording FROM %s WHERE dir=? AND msgnum=-1", odbc_table);
124                         res = SQLPrepare(stmt, (unsigned char *)sql, SQL_NTS);
125                         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
126                                 ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", sql);
127                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
128                                 break;
129                         }
130                         SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, strlen(dir), 0, (void *)dir, 0, NULL);
131                         res = odbc_smart_execute(obj, stmt);
132                         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
133                                 ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", sql);
134                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
135                                 break;
136                         }
137                         res = SQLFetch(stmt);
138                         if (res == SQL_NO_DATA) {
139                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
140                                 break;
141                         } else if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
142                                 ast_log(LOG_WARNING, "SQL Fetch error!\n[%s]\n\n", sql);
143                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
144                                 break;
145                         }
146                         fd = open(full_fn, O_RDWR | O_CREAT | O_TRUNC, 0770);
147                         if (fd < 0) {
148                                 ast_log(LOG_WARNING, "Failed to write '%s': %s\n", full_fn, strerror(errno));
149                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
150                                 break;
151                         }
152
153                         res = SQLGetData(stmt, 1, SQL_BINARY, NULL, 0, &colsize);
154                         fdlen = colsize;
155                         if (fd > -1) {
156                                 char tmp[1]="";
157                                 lseek(fd, fdlen - 1, SEEK_SET);
158                                 if (write(fd, tmp, 1) != 1) {
159                                         close(fd);
160                                         fd = -1;
161                                         break;
162                                 }
163                                 if (fd > -1)
164                                         fdm = mmap(NULL, fdlen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
165                         }
166                         if (fdm) {
167                                 memset(fdm, 0, fdlen);
168                                 res = SQLGetData(stmt, x + 1, SQL_BINARY, fdm, fdlen, &colsize);
169                                 if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
170                                         ast_log(LOG_WARNING, "SQL Get Data error!\n[%s]\n\n", sql);
171                                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
172                                         break;
173                                 }
174                         }
175                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
176                 } while (0);
177         } else
178                 ast_log(LOG_WARNING, "Failed to obtain database object for '%s'!\n", odbc_database);
179         if (fdm)
180                 munmap(fdm, fdlen);
181         if (fd > -1)
182                 close(fd);
183         return;
184 }
185 #endif
186
187 static char *convert(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 USE_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 USE_ODBC_STORAGE
282         retrieve_file(fn2);
283 #endif
284
285         if (ast_fileexists(fn, NULL, chan->language) > 0) {
286                 res = ast_stream_and_wait(chan, fn, chan->language, 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", chan->language, 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", chan->language, AST_DIGIT_ANY);
297                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
298                 }
299         }
300 #ifdef USE_ODBC_STORAGE
301         ast_filedelete(fn, NULL);       
302         ast_filedelete(fn2, NULL);      
303 #endif
304
305         for (loop = 3 ; loop > 0; loop--) {
306                 if (!res)
307                         res = ast_stream_and_wait(chan, "dir-instr", chan->language, AST_DIGIT_ANY);
308                 if (!res)
309                         res = ast_waitfordigit(chan, 3000);
310                 ast_stopstream(chan);
311         
312                 if (res < 0) /* User hungup, so jump out now */
313                         break;
314                 if (res == '1') {       /* Name selected */
315                         if (fromappvm) {
316                                 /* We still want to set the exten though */
317                                 ast_copy_string(chan->exten, ext, sizeof(chan->exten));
318                         } else {
319                                 if (ast_goto_if_exists(chan, dialcontext, ext, 1)) {
320                                         ast_log(LOG_WARNING,
321                                                 "Can't find extension '%s' in context '%s'.  "
322                                                 "Did you pass the wrong context to Directory?\n",
323                                                 ext, dialcontext);
324                                         res = -1;
325                                 }
326                         }
327                         break;
328                 }
329                 if (res == '*') /* Skip to next match in list */
330                         break;
331
332                 /* Not '1', or '*', so decrement number of tries */
333                 res = 0;
334         }
335
336         return(res);
337 }
338
339 static struct ast_config *realtime_directory(char *context)
340 {
341         struct ast_config *cfg;
342         struct ast_config *rtdata;
343         struct ast_category *cat;
344         struct ast_variable *var;
345         char *mailbox;
346         char *fullname;
347         char *hidefromdir;
348         char tmp[100];
349
350         /* Load flat file config. */
351         cfg = ast_config_load(VOICEMAIL_CONFIG);
352
353         if (!cfg) {
354                 /* Loading config failed. */
355                 ast_log(LOG_WARNING, "Loading config failed.\n");
356                 return NULL;
357         }
358
359         /* Get realtime entries, categorized by their mailbox number
360            and present in the requested context */
361         rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context, NULL);
362
363         /* if there are no results, just return the entries from the config file */
364         if (!rtdata)
365                 return cfg;
366
367         /* Does the context exist within the config file? If not, make one */
368         cat = ast_category_get(cfg, context);
369         if (!cat) {
370                 cat = ast_category_new(context);
371                 if (!cat) {
372                         ast_log(LOG_WARNING, "Out of memory\n");
373                         ast_config_destroy(cfg);
374                         return NULL;
375                 }
376                 ast_category_append(cfg, cat);
377         }
378
379         mailbox = NULL;
380         while ( (mailbox = ast_category_browse(rtdata, mailbox)) ) {
381                 fullname = ast_variable_retrieve(rtdata, mailbox, "fullname");
382                 hidefromdir = ast_variable_retrieve(rtdata, mailbox, "hidefromdir");
383                 snprintf(tmp, sizeof(tmp), "no-password,%s,hidefromdir=%s",
384                          fullname ? fullname : "",
385                          hidefromdir ? hidefromdir : "no");
386                 var = ast_variable_new(mailbox, tmp);
387                 if (var)
388                         ast_variable_append(cat, var);
389                 else
390                         ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
391         }
392         ast_config_destroy(rtdata);
393
394         return cfg;
395 }
396
397 static int do_directory(struct ast_channel *chan, struct ast_config *cfg, char *context, char *dialcontext, char digit, int last, int readext, int fromappvm)
398 {
399         /* Read in the first three digits..  "digit" is the first digit, already read */
400         char ext[NUMDIGITS + 1];
401         char name[80] = "";
402         struct ast_variable *v;
403         int res;
404         int found=0;
405         int lastuserchoice = 0;
406         char *start, *pos, *conv,*stringp=NULL;
407
408         if (ast_strlen_zero(context)) {
409                 ast_log(LOG_WARNING,
410                         "Directory must be called with an argument "
411                         "(context in which to interpret extensions)\n");
412                 return -1;
413         }
414         if (digit == '0') {
415                 if (!ast_goto_if_exists(chan, chan->context, "o", 1) ||
416                     (!ast_strlen_zero(chan->macrocontext) &&
417                      !ast_goto_if_exists(chan, chan->macrocontext, "o", 1))) {
418                         return 0;
419                 } else {
420                         ast_log(LOG_WARNING, "Can't find extension 'o' in current context.  "
421                                 "Not Exiting the Directory!\n");
422                         res = 0;
423                 }
424         }       
425         if (digit == '*') {
426                 if (!ast_goto_if_exists(chan, chan->context, "a", 1) ||
427                     (!ast_strlen_zero(chan->macrocontext) &&
428                      !ast_goto_if_exists(chan, chan->macrocontext, "a", 1))) {
429                         return 0;
430                 } else {
431                         ast_log(LOG_WARNING, "Can't find extension 'a' in current context.  "
432                                 "Not Exiting the Directory!\n");
433                         res = 0;
434                 }
435         }       
436         memset(ext, 0, sizeof(ext));
437         ext[0] = digit;
438         res = 0;
439         if (ast_readstring(chan, ext + 1, NUMDIGITS - 1, 3000, 3000, "#") < 0) res = -1;
440         if (!res) {
441                 /* Search for all names which start with those digits */
442                 v = ast_variable_browse(cfg, context);
443                 while(v && !res) {
444                         /* Find all candidate extensions */
445                         while(v) {
446                                 /* Find a candidate extension */
447                                 start = strdup(v->value);
448                                 if (start && !strcasestr(start, "hidefromdir=yes")) {
449                                         stringp=start;
450                                         strsep(&stringp, ",");
451                                         pos = strsep(&stringp, ",");
452                                         if (pos) {
453                                                 ast_copy_string(name, pos, sizeof(name));
454                                                 /* Grab the last name */
455                                                 if (last && strrchr(pos,' '))
456                                                         pos = strrchr(pos, ' ') + 1;
457                                                 conv = convert(pos);
458                                                 if (conv) {
459                                                         if (!strcmp(conv, ext)) {
460                                                                 /* Match! */
461                                                                 found++;
462                                                                 free(conv);
463                                                                 free(start);
464                                                                 break;
465                                                         }
466                                                         free(conv);
467                                                 }
468                                         }
469                                         free(start);
470                                 }
471                                 v = v->next;
472                         }
473
474                         if (v) {
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);
477                                 switch (res) {
478                                         case -1:
479                                                 /* user pressed '1' but extension does not exist, or
480                                                  * user hungup
481                                                  */
482                                                 lastuserchoice = 0;
483                                                 break;
484                                         case '1':
485                                                 /* user pressed '1' and extensions exists;
486                                                    play_mailbox_owner will already have done
487                                                    a goto() on the channel
488                                                  */
489                                                 lastuserchoice = res;
490                                                 break;
491                                         case '*':
492                                                 /* user pressed '*' to skip something found */
493                                                 lastuserchoice = res;
494                                                 res = 0;
495                                                 break;
496                                         default:
497                                                 break;
498                                 }
499                                 v = v->next;
500                         }
501                 }
502
503                 if (lastuserchoice != '1') {
504                         res = ast_streamfile(chan, found ? "dir-nomore" : "dir-nomatch", chan->language);
505                         if (!res)
506                                 res = 1;
507                         return res;
508                 }
509                 return 0;
510         }
511         return res;
512 }
513
514 static int directory_exec(struct ast_channel *chan, void *data)
515 {
516         int res = 0;
517         struct localuser *u;
518         struct ast_config *cfg;
519         int last = 1;
520         int readext = 0;
521         int fromappvm = 0;
522         char *dirintro, *parse;
523         AST_DECLARE_APP_ARGS(args,
524                 AST_APP_ARG(vmcontext);
525                 AST_APP_ARG(dialcontext);
526                 AST_APP_ARG(options);
527         );
528
529         if (ast_strlen_zero(data)) {
530                 ast_log(LOG_WARNING, "Directory requires an argument (context[,dialcontext])\n");
531                 return -1;
532         }
533
534         LOCAL_USER_ADD(u);
535
536         parse = ast_strdupa(data);
537
538         AST_STANDARD_APP_ARGS(args, parse);
539                 
540         if (args.options) {
541                 if (strchr(args.options, 'f'))
542                         last = 0;
543                 if (strchr(args.options, 'e'))
544                         readext = 1;
545                 if (strchr(args.options, 'v'))
546                         fromappvm = 1;
547         }
548
549         if (ast_strlen_zero(args.dialcontext))  
550                 args.dialcontext = args.vmcontext;
551
552         cfg = realtime_directory(args.vmcontext);
553         if (!cfg) {
554                 ast_log(LOG_ERROR, "Unable to read the configuration data!\n");
555                 LOCAL_USER_REMOVE(u);
556                 return -1;
557         }
558
559         dirintro = ast_variable_retrieve(cfg, args.vmcontext, "directoryintro");
560         if (ast_strlen_zero(dirintro))
561                 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
562         if (ast_strlen_zero(dirintro))
563                 dirintro = last ? "dir-intro" : "dir-intro-fn";
564
565         if (chan->_state != AST_STATE_UP) 
566                 res = ast_answer(chan);
567
568         for (;;) {
569                 if (!res)
570                         res = ast_stream_and_wait(chan, dirintro, chan->language, AST_DIGIT_ANY);
571                 ast_stopstream(chan);
572                 if (!res)
573                         res = ast_waitfordigit(chan, 5000);
574                 if (res > 0) {
575                         res = do_directory(chan, cfg, args.vmcontext, args.dialcontext, res, last, readext, fromappvm);
576                         if (res > 0) {
577                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
578                                 ast_stopstream(chan);
579                                 if (res >= 0)
580                                         continue;
581                         }
582                 }
583                 break;
584         }
585         ast_config_destroy(cfg);
586         LOCAL_USER_REMOVE(u);
587         return res;
588 }
589
590 static int unload_module(void *mod)
591 {
592         int res;
593         res = ast_unregister_application(app);
594         return res;
595 }
596
597 static int load_module(void *mod)
598 {
599         __mod_desc = mod;
600 #ifdef USE_ODBC_STORAGE
601         struct ast_config *cfg = ast_config_load(VOICEMAIL_CONFIG);
602         char *tmp;
603
604         if (cfg) {
605                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbcstorage"))) {
606                         ast_copy_string(odbc_database, tmp, sizeof(odbc_database));
607                 }
608                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbctable"))) {
609                         ast_copy_string(odbc_table, tmp, sizeof(odbc_table));
610                 }
611                 if ((tmp = ast_variable_retrieve(cfg, "general", "format"))) {
612                         ast_copy_string(vmfmts, tmp, sizeof(vmfmts));
613                 }
614                 ast_config_destroy(cfg);
615         } else
616                 ast_log(LOG_WARNING, "Unable to load " VOICEMAIL_CONFIG " - ODBC defaults will be used\n");
617 #endif
618
619         return ast_register_application(app, directory_exec, synopsis, descrip);
620 }
621
622 static const char *description(void)
623 {
624         return "Extension Directory";
625 }
626
627 static const char *key(void)
628 {
629         return ASTERISK_GPL_KEY;
630 }
631
632 STD_MOD1;