Index: apps/app_vmoutcall.c =================================================================== --- apps/app_vmoutcall.c (.../vendor/head/current) (revision 0) +++ apps/app_vmoutcall.c (.../tags/vmoutcall-0.4) (revision 66) @@ -0,0 +1,541 @@ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../astconf.h" + +static char *tdesc = "Periodically call Voice Mail users to remind them of new messages."; + +static char *config_filename = "vmoutcall.conf"; +struct ast_config *config; +AST_MUTEX_DEFINE_STATIC(config_lock); + +static int quitting = 0; + +static struct sched_context *sched = NULL; +static pthread_t check_thread_id = AST_PTHREADT_NULL; + +/* + * Check voice mail boxes to see if they have new messages. + */ + +static int check(void *data) +{ + struct ast_variable *v; + + char *category; + char *category_next; + + char mailbox[256]; + char mailbox_name[256]; + char mailbox_context[256]; + char mailbox_greet[256]; + char mailbox_greet_full[256]; + + int found; + int allow; + + int newmsgs = 0; + int oldmsgs = 0; + + char dbkey[256]; + char dbvalue[256]; + + char *value; + + long last_check = 0; + long last_notify = 0; + long check_interval = 60; + long notify_interval = 3600; + long random_interval = 300; + + struct ast_timing timing; + + char tmppath[256]; + char path[256]; + FILE *file; + + struct stat st; + + time_t current_time; + time_t notify_time; + struct utimbuf ut; + + ast_log(LOG_DEBUG, "VMOutCall is beginning check of mailboxes.\n"); + + /* Load the config file each time we run the check. Unless there + * are thousands of entries in the config file performance shouldn't + * be a problem. */ + + ast_mutex_lock(&config_lock); + if (!config) { + ast_mutex_unlock(&config_lock); + ast_log(LOG_ERROR, "Configuration has not been loaded."); + return -1; + } + + /* Scan through all of the categories in the config file, looking + * for categories with '@' in them. These are the sections for + * individal users. + */ + + category_next = ast_category_browse(config, NULL); + while (category_next) { + category = category_next; + category_next = ast_category_browse(config, category); + + if (!(value = ast_variable_retrieve(config, category, "mailbox"))) { + ast_log(LOG_DEBUG, "Category %s does not describe a mailbox.\n", category); + continue; + } + + if (ast_strlen_zero(value)) { + ast_log(LOG_ERROR, "Zero length mailbox name in category %s.\n", category); + continue; + } + + /* save the mailbox */ + strncpy(mailbox, value, sizeof(mailbox)); + + if (!(value = strchr(mailbox, '@'))) { + strncpy(mailbox_name, mailbox, sizeof(mailbox_name)); + strncpy(mailbox_context, "default", sizeof(mailbox_context)); + } else { + strncpy(mailbox_name, mailbox, (value - mailbox)); + strncpy(mailbox_context, value + 1, sizeof(mailbox_context)); + } + + snprintf(mailbox_greet, sizeof(mailbox_greet), "%s/voicemail/%s/%s/greet", ast_config_AST_SPOOL_DIR, mailbox_context, mailbox_name); + snprintf(mailbox_greet_full, sizeof(mailbox_greet_full), "%s.gsm", mailbox_greet); + + if (ast_strlen_zero(mailbox_name)) { + ast_log(LOG_ERROR, "Zero length mailbox name."); + continue; + } + + if (ast_strlen_zero(mailbox_context)) { + ast_log(LOG_ERROR, "Zero length mailbox context."); + continue; + } + + ast_log(LOG_DEBUG, "Processing category %s.\n", category); + + found = 0; + allow = 0; + + v = ast_variable_browse(config, category); + while (v) { + if (!strcasecmp(v->name, "allow_calls")) { + found = 1; + if (ast_build_timing(&timing, v->value) && ast_check_timing(&timing)) { + allow = 1; + break; + } + } else if (!strcasecmp(v->name, "deny_calls")) { + found = 1; + if (ast_build_timing(&timing, v->value) && ast_check_timing(&timing)) { + allow = 0; + break; + } + } + v = v->next; + } + + if (!found) { + v = ast_variable_browse(config, mailbox_context); + while (v) { + if (!strcasecmp(v->name, "allow_calls")) { + found = 1; + if (ast_build_timing(&timing, v->value) && ast_check_timing(&timing)) { + allow = 1; + break; + } + } else if (!strcasecmp(v->name, "deny_calls")) { + found = 1; + if (ast_build_timing(&timing, v->value) && ast_check_timing(&timing)) { + allow = 0; + break; + } + } + v = v->next; + } + } + + if (!found) { + v = ast_variable_browse(config, "general"); + while (v) { + if (!strcasecmp(v->name, "allow_calls")) { + found = 1; + if (ast_build_timing(&timing, v->value) && ast_check_timing(&timing)) { + allow = 1; + break; + } + } else if (!strcasecmp(v->name, "deny_calls")) { + found = 1; + if (ast_build_timing(&timing, v->value) && ast_check_timing(&timing)) { + allow = 0; + break; + } + } + v = v->next; + } + } + + if (found && !allow) { + ast_log(LOG_NOTICE, "Outgoing calls for category %s are not allowed at this time.\n", category); + continue; + } + + ast_log(LOG_DEBUG, "Outgoing calls for category %s are allowed at this time.\n", category); + + check_interval = 60; + if ((value = ast_variable_retrieve(config, category, "check_interval")) || + (value = ast_variable_retrieve(config, mailbox_context, "check_interval")) || + (value = ast_variable_retrieve(config, "general", "check_interval"))) { + check_interval = atoi(value); + } + if (check_interval < 30) { + check_interval = 30; + } + + ast_log(LOG_DEBUG, "Check interval for category %s is %li seconds.\n", category, check_interval); + + /* get the time that we last checked this mailbox */ + snprintf(dbkey, sizeof(dbkey), "%s/last_check", category); + if (ast_db_get("vmoutcall", dbkey, dbvalue, sizeof(dbvalue))) { + ast_log(LOG_DEBUG, "Could get not time of last check for mailbox %s - first time checking this mailbox?\n", mailbox); + last_check = 0; + } else { + last_check = atol(dbvalue); + } + + if ((time(NULL) - last_check) < check_interval) { + ast_log(LOG_DEBUG, "Category %s does not need checking at this time.\n", category); + continue; + } + + /* get the message counts */ + if (ast_app_messagecount(mailbox, &newmsgs, &oldmsgs)) { + ast_log(LOG_ERROR, "Could not get message count for mailbox %s\n", mailbox); + continue; + } + + /* save the time of the check */ + snprintf(dbvalue, sizeof(dbvalue), "%li", time(NULL)); + if (ast_db_put("vmoutcall", dbkey, dbvalue)) { + ast_log(LOG_ERROR, "Could not save time of last check for category %s.\n", category); + continue; + } + + ast_log(LOG_DEBUG, "Mailbox %s has %i new and %i old messages.\n", mailbox, newmsgs, oldmsgs); + + if (newmsgs == 0) { + ast_log(LOG_DEBUG, "No new messages in mailbox %s.\n", mailbox); + continue; + } + + notify_interval = 3600; + if ((value = ast_variable_retrieve(config, category, "notify_interval")) || + (value = ast_variable_retrieve(config, mailbox_context, "notify_interval")) || + (value = ast_variable_retrieve(config, "general", "notify_interval"))) { + notify_interval = atoi(value); + } + if (notify_interval < 300) { + notify_interval = 300; + } + + ast_log(LOG_DEBUG, "Notify interval for category %s is %li seconds.\n", category, notify_interval); + + snprintf(dbkey, sizeof(dbkey), "%s/last_notify", category); + if (ast_db_get("vmoutcall", dbkey, dbvalue, sizeof(dbvalue))) { + ast_log(LOG_DEBUG, "Could get not time of last notify for category %s - first time notifying this mailbox?\n", mailbox); + last_notify = 0; + } else { + last_notify = atol(dbvalue); + } + + if ((time(NULL) - last_notify) < notify_interval) { + ast_log(LOG_DEBUG, "It is too soon after the last notification to notify category %s again.\n", mailbox); + continue; + } + + snprintf(tmppath, sizeof(tmppath), "%s/%s/vmoutcall-%s.call", ast_config_AST_SPOOL_DIR, "tmp", category); + snprintf(path, sizeof(path), "%s/%s/vmoutcall-%s.call", ast_config_AST_SPOOL_DIR, "outgoing", category); + + /* write out the call file */ + file = fopen(tmppath, "w"); + + if (!file) { + ast_log(LOG_ERROR, "Could not open file %s for writing.\n", tmppath); + continue; + } + + if ((value = ast_variable_retrieve(config, category, "channel")) || + (value = ast_variable_retrieve(config, mailbox_context, "channel")) || + (value = ast_variable_retrieve(config, "general", "channel"))) { + fprintf(file, "Channel: %s\n", value); + } + + if ((value = ast_variable_retrieve(config, category, "application")) || + (value = ast_variable_retrieve(config, mailbox_context, "application")) || + (value = ast_variable_retrieve(config, "general", "application"))) { + fprintf(file, "Application: %s\n", value); + } + + if ((value = ast_variable_retrieve(config, category, "data")) || + (value = ast_variable_retrieve(config, mailbox_context, "data")) || + (value = ast_variable_retrieve(config, "general", "data"))) { + fprintf(file, "Data: %s\n", value); + } + + if ((value = ast_variable_retrieve(config, category, "callerid")) || + (value = ast_variable_retrieve(config, mailbox_context, "callerid")) || + (value = ast_variable_retrieve(config, "general", "callerid"))) { + fprintf(file, "CallerID: %s\n", value); + } + + if ((value = ast_variable_retrieve(config, category, "maxretries")) || + (value = ast_variable_retrieve(config, mailbox_context, "maxretries")) || + (value = ast_variable_retrieve(config, "general", "maxretries"))) { + fprintf(file, "MaxRetries: %s\n", value); + } else { + fprintf(file, "MaxRetries: 0\n"); + } + + if ((value = ast_variable_retrieve(config, category, "retrytime")) || + (value = ast_variable_retrieve(config, mailbox_context, "retrytime")) || + (value = ast_variable_retrieve(config, "general", "retrytime"))) { + fprintf(file, "RetryTime: %s\n", value); + } else { + fprintf(file, "RetryTime: 120\n"); + } + + if ((value = ast_variable_retrieve(config, category, "waittime")) || + (value = ast_variable_retrieve(config, mailbox_context, "waittime")) || + (value = ast_variable_retrieve(config, "general", "waittime"))) { + fprintf(file, "WaitTime: %s\n", value); + } else { + fprintf(file, "WaitTime: 60\n"); + } + + if ((value = ast_variable_retrieve(config, category, "context")) || + (value = ast_variable_retrieve(config, mailbox_context, "context")) || + (value = ast_variable_retrieve(config, "general", "context"))) { + fprintf(file, "Context: %s\n", value); + } else { + fprintf(file, "Context: vmoutcall\n"); + } + + if ((value = ast_variable_retrieve(config, category, "extension")) || + (value = ast_variable_retrieve(config, mailbox_context, "extension")) || + (value = ast_variable_retrieve(config, "general", "extension"))) { + fprintf(file, "Extension: %s\n", value); + } else { + fprintf(file, "Extension: s\n"); + } + + if ((value = ast_variable_retrieve(config, category, "priority")) || + (value = ast_variable_retrieve(config, mailbox_context, "priority")) || + (value = ast_variable_retrieve(config, "general", "priority"))) { + fprintf(file, "Priority: %s\n", value); + } else { + fprintf(file, "Priority: 1\n"); + } + + fprintf(file, "SetVar: MAILBOX=%s\n", mailbox); + fprintf(file, "SetVar: MAILBOX_NAME=%s\n", mailbox_name); + fprintf(file, "SetVar: MAILBOX_CONTEXT=%s\n", mailbox_context); + if(!stat(mailbox_greet_full, &st)) { + fprintf(file, "SetVar: MAILBOX_GREET=%s\n", mailbox_greet); + } + fprintf(file, "SetVar: NEWMSGS=%i\n", newmsgs); + fprintf(file, "SetVar: OLDMSGS=%i\n", oldmsgs); + + v = ast_variable_browse(config, category); + while (v) { + if (!strcasecmp(v->value, "setvar")) { + fprintf(file, "SetVar: %s\n", v->value); + } + v = v->next; + } + + v = ast_variable_browse(config, mailbox_context); + while (v) { + if (!strcasecmp(v->value, "setvar")) { + fprintf(file, "SetVar: %s\n", v->value); + } + v = v->next; + } + + v = ast_variable_browse(config, "general"); + while (v) { + if (!strcasecmp(v->value, "setvar")) { + fprintf(file, "SetVar: %s\n", v->value); + } + v = v->next; + } + + fclose(file); + + /* pick a time a short random time in the future and schedule the outgoing call then */ + + random_interval = 60; + if ((value = ast_variable_retrieve(config, category, "random_interval")) || + (value = ast_variable_retrieve(config, mailbox_context, "random_interval")) || + (value = ast_variable_retrieve(config, "general", "random_interval"))) { + random_interval = atoi(value); + } + if (random_interval < 0) { + random_interval = 0; + } else if (random_interval > notify_interval) { + random_interval = notify_interval; + } + + current_time = time(NULL); + notify_time = current_time + (random() % random_interval) + 1; + + ut.actime = current_time; + ut.modtime = notify_time; + + if (utime(tmppath, &ut)) { + ast_log(LOG_ERROR, "Unable to set access/modification times for %s\n", tmppath); + /* log the error but keep on going */ + } + + if (rename(tmppath, path)) { + ast_log(LOG_ERROR, "Unable to rename from %s to %s: %s\n", tmppath, path, strerror(errno)); + continue; + } + + /* save the time of the notify */ + snprintf(dbvalue, sizeof(dbvalue), "%li", notify_time); + if (ast_db_put("vmoutcall", dbkey, dbvalue)) { + ast_log(LOG_ERROR, "Could not save time of last notify for mailbox %s.\n", mailbox); + } + } + + ast_mutex_unlock(&config_lock); + + ast_sched_add(sched, 30000, check, 0); + + return 0; +} + +static void *check_thread(void *ignore) +{ + int res; + + while (!quitting) { + res = ast_sched_wait(sched); + if (res >= 0) { + ast_sched_runq(sched); + } + } + return NULL; +} + +int reload(void) +{ + struct ast_config *config_temp; + + config_temp = ast_load(config_filename); + if (!config_temp) { + ast_log(LOG_ERROR, "Unable to load config file %s", config_filename); + return -1; + } + + ast_mutex_lock(&config_lock); + ast_destroy(config); + config = config_temp; + ast_mutex_unlock(&config_lock); + + return 0; +} + +int unload_module(void) +{ + if (check_thread_id) { + quitting = 1; + pthread_join(check_thread_id, NULL); + check_thread_id = AST_PTHREADT_NULL; + } + ast_mutex_lock(&config_lock); + ast_destroy(config); + config = NULL; + ast_mutex_unlock(&config_lock); + + return 0; +} + +int load_module(void) +{ + struct ast_config *config_temp; + + quitting = 0; + + config_temp = ast_load(config_filename); + if (!config_temp) { + ast_log(LOG_ERROR, "Unable to load config file %s", config_filename); + return -1; + } + + ast_mutex_lock(&config_lock); + config = config_temp; + ast_mutex_unlock(&config_lock); + + sched = sched_context_create(); + if (!sched) { + ast_log(LOG_ERROR, "Out of memory.\n"); + return -1; + } + + /* schedule the first check in 1 second */ + if (!ast_sched_add(sched, 1000, check, 0)) { + ast_log(LOG_ERROR, "Unable to schedule 1st check."); + return -1; + } + + /* create a thread to run the checks in */ + if (ast_pthread_create(&check_thread_id, NULL, check_thread, NULL)) { + ast_log(LOG_ERROR, "Unable to create check thread."); + return -1; + } + + return 0; +} + +char *description(void) +{ + return tdesc; +} + +int usecount(void) +{ + return 0; +} + +char *key() +{ + return ASTERISK_GPL_KEY; +} Index: apps/Makefile =================================================================== --- apps/Makefile (.../vendor/head/current) (revision 66) +++ apps/Makefile (.../tags/vmoutcall-0.4) (revision 66) @@ -32,6 +32,8 @@ app_test.so app_forkcdr.so app_math.so app_realtime.so \ app_dumpchan.so +APPS+=app_vmoutcall.so + ifneq (${OSARCH},Darwin) APPS+=app_intercom.so endif Index: configs/vmoutcall.conf.sample =================================================================== --- configs/vmoutcall.conf.sample (.../vendor/head/current) (revision 0) +++ configs/vmoutcall.conf.sample (.../tags/vmoutcall-0.4) (revision 66) @@ -0,0 +1,98 @@ +; +; vmoutcall.conf +; + +; This configuration file is organized into a number of different +; sectiions. The "general" section contains global settings. There +; can be a section for each voicemail context that you have that can +; be used to set defaults for mailboxes in that voicemail context. +; Then there can be sections for individual mailboxes. + +; The "general" section is always called "general". You can distinguish +; sections that specify defaults for an entire voicemail context by the +; presense of a "mailbox" parameter. +; +; mailbox = 01@default +; mailbox = 01 ; if no context is specified "default" is assumed +; +; The following settings control the frequency that mailboxes are +; checked and notifications are sent out. +; +; check_interval - time in seconds between checks for new voicemail - default 60 +; notify_interval - minimum time in seconds between outgoing calls - default 3600 +; random_interval - short random interval added to notify interval so that +; a large batch of calls don't go out at once - default 60 +; +; +; To permit outgoing calls only during specific times, "allow_calls" +; and "deny_calls" parameters should be specified. Outgoing calls +; will be permitted if any "allow_calls" parameter matches and no +; "deny_calls" parameter matches. The "allow_calls" and "deny_calls" +; parameters are checked in order until one matches. If the +; individual mailbox section does not contain any "allow_calls" or +; "deny_calls" parameters the section for the voicemail context and +; the general section will be checked in a similar manner. If no +; parameters are found outgoing calls are permitted. +; +; allow_calls = *|*|*|* +; deny_calls = *|*|*|* +; +; The following settings are used to construct the call file. You must +; either specify a channel or an application and data. +; +; channel = Local/5551212@outgoing +; application = +; data = +; +; Setting the CallerID is optional. +; +; callerid = "Voice Mail Outcall" <12125553000> +; +; The following settings are optional. If they are omitted the following default +; values will be supplied. +; +; maxretries = 0 +; retrytime = 120 +; waittime = 60 +; context = vmoutcall +; extension = s +; priority = 1 +; +; The following variables will be available to the script that handles the outgoing call: +; +; MAILBOX = the full mailbox - name@context +; MAILBOX_NAME = the name of the mailbox +; MAILBOX_CONTEXT = the voicemail context to which the mailbox belongs +; MAILBOX_GREET = the path to the sound file that contains the name recorded by the user +; NEWMSGS = the number of new messages +; OLDMSGS = the number of old messages +; +; Other variables can be passed to the script by using "setvar": +; +; setvar = NAME=VALUE + + +; settings in the general section apply to everyone +[general] +callerid = "Voice Mail Outcall" <12125553000> + +; then there are settings for each voice mail context +[default] +notify_interval = 900 + +; call this person on cell phone during work +[01@default-work] + +mailbox = 01@default +allow_calls = 09:00-17:00|mon-fri|*|* +channel = Local/5551000@outgoing + +; and at all other times, call this person's home number +[01@default-home] + +mailbox = 01@default +deny_calls = *|*|25|dec +allow_calls = 17:00-22:00|mon-fri|*|* +allow_calls = 09:00-19:00|sat|*|* + +channel = Local/5552000@outgoing Index: configs/extensions.conf.sample =================================================================== --- configs/extensions.conf.sample (.../vendor/head/current) (revision 66) +++ configs/extensions.conf.sample (.../tags/vmoutcall-0.4) (revision 66) @@ -421,3 +421,34 @@ ; 'show application ' will show details of how you ; use that particular application in this file, the dial plan. ; +; Sample script for the vmoutcall application. +; +;[vmoutcall] +; +;exten => s,1,Answer +;exten => s,2,SetVar(count=0) +;exten => s,3,GotoIf($[${count} > 3]?*,1) +;exten => s,4,Background(this-is-the-voice-mail-system) ; *** need to get this recorded *** +;exten => s,5,Background(the-mailbox) ; *** need to get this recorded *** +;exten => s,6,GotoIf($["${MAILBOX_GREET}" = ""]?10) +;exten => s,7,Background(for) ; *** need to get this recorded *** +;exten => s,8,Background(${MAILBOX_GREET}) +;exten => s,9,Goto(11) +;exten => s,10,SayAlpha(${MAILBOX_NAME}) +;exten => s,11,Background(has) ; *** need to get this recorded *** +;exten => s,12,SayNumber(${NEWMSGS}) +;exten => s,13,Background(vm-INBOX) +;exten => s,14,GotoIf($[${NEWMSGS} = 1]?17) +;exten => s,15,Background(vm-messages) +;exten => s,16,Goto(18) +;exten => s,17,Background(vm-message) +;exten => s,18,Background(press-pound-to-login-to-voicemail-or-star-to-hangup) ; *** need to get this recorded *** +;exten => s,19,SetVar(count=$[${count} + 1]) +; +;exten => t,1,Goto(s,3) +; +;exten => *,1,Hangup +; +;exten => _#,1,VoiceMailMain(${MAILBOX}) +; +;exten => i,1,Goto(s,3)