Capisuite: Unterschied zwischen den Versionen

Aus Neobikers Wiki
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
 
(35 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
[[Neobiker%27s_Wiki:Portal|Zurück zum Portal]]
== Capisuite Erweiterung ==
== Capisuite Erweiterung ==


Bei der Umstellung von VBox unter Debian Sarge auf Capisuite unter Etch habe ich schmerzlich die Möglichkeit der individuellen Programmierung der Anrufbeantworter vermisst. Nachdem ich keine verbesserten ''Incoming.py'' Files im Web gefunden habe, habe ich auf die schnelle Python gelernt und die fehlende Funktion selbst implementiert.  
Bei der Umstellung von VBox unter Debian Sarge auf Capisuite unter Etch habe ich schmerzlich die Möglichkeit der individuellen '''Programmierung des Anrufbeantworters''' vermisst. Nachdem ich keine verbesserten ''incoming.py'' Files im Web gefunden habe, habe ich auf die schnelle Python gelernt und die fehlende Funktion selbst implementiert.
 
'''Individuelle Programmierung'''
 
In den Konfigfiles können Programme hinterlegt werden, die in Abhängigkeit von Zeit, Datum oder Telefonnummern (des Anrufers oder der Zielnummer) unterschiedlich reagieren. Ich nutze z.B. je eine eigene MSN für Privates, Geschäftliches und Fax. Die Geschäftsnummer klingelt nur Mo-Fr zw. 8:30 und 17:00, sonst aktiviert sich direkt der AB mit einer geschäftlichen Ansage. Wochenenden, Feiertage und Geburtstage sind zusätzlich hinterlegt. Familie und Freunde können uns Abends z.B. länger erreichen, als unbekannte Anrufer, die Nachrichten werden auch länger aufgezeichnet. Die Möglichkeiten sind nahezu beliebig.


'''Programmierung'''
* Die ''Programme'' werden '''fortlaufend (!)''' nummeriert, beginnend bei '''''prog1'''''
* Die Programme werden '''fortlaufend (!)''' nummeriert, beginnend bei '''''prog1'''''
* Das erste Programm ''prog1 ... progX'' das ''match'ed'' (Zeit, Datum, etc.) wird verwendet, deshalb ist die '''Reihenfolge''' relevant!
* Das erste Programm ''prog1 ... progX'' das ''match'ed'' wird verwendet, deshalb ist die '''Reihenfolge''' relevant!
* Die Einträge '''dates''' und '''group''' können als ''eigene Section'' im Konfigfile definiert werden (siehe holiday, family, friends)
* Die Einträge '''dates''' und '''group''' können als ''eigene Section'' definiert werden (siehe holiday, family, friends)
* Die Telefonnummern müssen so definiert sein, wie sie im Logfile (''/var/log/capisuite.log'') erscheinen, d.h. üblicherweise '''0911 12345678''' (nicht: +49 (911) 12345678).
* Die Telefonnummern müssen so definiert sein, wie sie im Logfile (''/var/log/capisuite.log'') erscheinen, d.h. üblicherweise '''0911 12345678''' (nicht: +49 (911) 12345678).
* Es kann ein eigenes Telefonbuch verwendet werden. Einträge darin werden mit Namen gelistet (Email/Fax Email enthält den Namen und die Nummer)




Zeile 22: Zeile 28:


[priv]
[priv]
voice_numbers="20"
voice_numbers="12345678"
announcement="ab_priv.la"
announcement="ab_priv.la"
voice_action="MailAndSave"
voice_action="MailAndSave"
Zeile 28: Zeile 34:
record_length="90"
record_length="90"
voice_email_from="ab@neobiker.de"
voice_email_from="ab@neobiker.de"
voice_email="Anrufbeantworter <ab@neobiker.de>"
voice_email="Anrufbeantworter (Priv) <ab@neobiker.de>"
pin="1111"
pin="1111"
# ----- vbox programming:
# ----- vbox programming:
Zeile 38: Zeile 44:


[buis]
[buis]
voice_numbers="23"
voice_numbers="22233345"
announcement="ab_buis.la"
announcement="ab_buis.la"
voice_action="MailAndSave"
voice_action="MailAndSave"
voice_delay="10"
voice_delay="10"
record_length="60"
record_length="60"
voice_email_from="Anrufbeantworter <dr@friedrichnet.de>"
voice_email_from="Anrufbeantworter (Buis) <ab@neobiker.de>"
voice_email="ab@friedrichnet.de"
voice_email="ab@neobiker.de"
pin="2222"
pin="2222"
# ----- vbox programming:
# ----- vbox programming:
Zeile 60: Zeile 66:
Sonstige=1.1.,6.1.,1.5.,3.10.,1.11.
Sonstige=1.1.,6.1.,1.5.,3.10.,1.11.
Birthdays=19.5.
Birthdays=19.5.
</pre>
Zusätzlich habe ich ein '''Telefonbuch''' definiert, in welchem auch die verschiedenen ''Gruppen'' (s.o. das Konfigfile / AB-Programme) definiert sind (Familie, Freunde, ...): ''/etc/capisuit/phonebook.conf''
<pre>
[GLOBAL]
Privat = 123456
Geschäft = 123457
Fax = 123458


[family]
[family]
Zeile 67: Zeile 81:
[friends]
[friends]
Karl=12345,017134566789
Karl=12345,017134566789
Peter=0170123456
Peter=0170123456, 0911 123412
 
[others]
Arzt = 12345
Werkstatt = 12346789
Buero = 123 345 567
</pre>
Telefonnummern, welche nicht im Telefonbuch stehen, werden per '''online reverse lookup'''  unter ''www.dasoertliche.de'' nachgeschlagen und sofern gefunden unter ''/var/cache/tbident/phonebook.cache'' gespeichert.
 
Der Anrufbeantworter (Fax dito) sendet mir eine '''Email''' mit einer Wav-Datei (oder PDF bei Fax) und den wichtigsten Info's:
<pre>
From:    Anrufbeantworter (Priv) <ab@neobiker.de>
To:      ab@neobiker.de
Subject: Nachricht von Unbekannt fuer Privat
Anlage:  voice-184.wav
 
Anrufer: Unbekannt (0911987654321)
       
Laenge:  31 Sek.
Datum:  Di 11 Sep 2007 13:57:43 CEST
Nummer:  Privat (123456)
 
Siehe Anhang.
Das File wurde gespeichert unter: /var/spool/capisuite/users/ab/received/voice-184.la
</pre>
</pre>
=== Downloads ===
[http://www.neobiker.de/ftp/pub/isdn/ Download ISDN Files]


== Erweiterte Scripts ==
== Erweiterte Scripts ==
Zeile 75: Zeile 115:
* incoming_adv.py
* incoming_adv.py
* idle_adv.py
* idle_adv.py
erstellt. Diese müssen in ''/etc/capisuite/capisuite.conf'' aktiviert werden.
* tbident.sh
erstellt. Die ersten beiden werden in ''/etc/capisuite/capisuite.conf'' aktiviert, das letzte zur Telefonbuch-Identifikation (tbident.sh) muss im Standardpfad installiert werden (z.B. unter ''/usr/bin/tbident.sh'').
 
Die Scripts sind für eine Deutsche Ausgabe optimiert worden. Es wird einmal setlocale(de_DE) verwendet. Deshalb sind auf dem System die deutschen locales notwendig, diese können unter Debian mit '''dpkg-reconfigure locales''' erzeugt werden.
 
'''Hinweis''': Die Scripts müssen zuerst compiliert werden!


Folgender Batch compiliert die Python Scripts anschliessend:
Folgendes Script compiliert die CapiSuite Python Scripts:
<pre>
<pre>
#!/bin/sh
#!/bin/sh
PYTHON=python2.3
PYTHON=python2.4
DIRLIST=" /usr/lib/capisuite "
DIRLIST=" /usr/lib/capisuite "
for i in $DIRLIST ; do
for i in $DIRLIST ; do
Zeile 88: Zeile 133:
</pre>
</pre>


File: ''/usr/lib/capisuite/incoming_adv.py''
=== File: ''/usr/lib/capisuite/incoming_adv.py'' ===
[[incoming_adv|Hier]] ist das erweiterte File ''/usr/lib/capisuite/incoming_adv.py''.
 
=== File: ''/usr/lib/capisuite/idle_adv.py'' ===
Das ''idle.py'' File benötigt eine kleine Änderung (ganz unten im Script, mit '+' markiert), damit die neuen Sections nicht zu einer Fehlermeldung (über nicht existierenden System-User) führen.
 
<pre>
<pre>
#             incoming_adv.py - advanced incoming script for capisuite
#               idle_adv.py - default script for capisuite
#              --------------------------------------------------------
#              ---------------------------------------------
#    copyright            : (c) 2007 by Neobiker
#    copyright            : (C) 2007 neobiker
#    Version              : 1.1
#    Date                : 03.01.2007
#
#
#    original script:
#    original script:
#    copyright            : (C) 2002 by Gernot Hillier
#    copyright            : (C) 2002 by Gernot Hillier
#    email                : gernot@hillier.de
#    email                : gernot@hillier.de
#    version              : $Revision: 1.9.2.1 $
#    version              : Revision: 1.8.2.2
#
#
#  This program is free software; you can redistribute it and/or modify
#  This program is free software; you can redistribute it and/or modify
Zeile 105: Zeile 157:
#
#


# general imports
import os,re,time,pwd,fcntl
import datetime,time,os,re,string,pwd
# capisuite stuff
# CapiSuite imports
import capisuite,cs_helpers
import capisuite,cs_helpers


# @brief check if user program date fits actual date
def idle(capi):
#
        config=cs_helpers.readConfig()
# @param pdate user program date
        spool=cs_helpers.getOption(config,"","spool_dir")
# @param adate actual date
        if (spool==None):
                capisuite.error("global option spool_dir not found.")
                return


def check_date (pdate,adate):
        done=os.path.join(spool,"done")+"/"
    """check_date() check if actual date fits user program date"""
         failed=os.path.join(spool,"failed")+"/"
    if pdate == '*':
         return True
    # set day/month/year from actual date if field is not defined
    idate=pdate.split('.')
    pdate=adate[:]
    # set pdate fields day/month/year as defined in user program
    for i in range(len(idate)):
        if idate[i] and idate[i] != '*': pdate[i] = int(idate[i])
    if pdate == adate:
        if adv_debug:
            print "check_date(): " , pdate , " / " , adate
        return True
    return False


# @brief check if date is listed in date section
        if (not os.access(done,os.W_OK) or not os.access(failed,os.W_OK)):
#
                capisuite.error("Can't read/write to the necessary spool dirs")
# @param dsect date section list of dates
                return


def check_date_section (dsect,adate):
        userlist=config.sections()
    """check_date_section () check if date is listed in date section"""
         userlist.remove('GLOBAL')
    for i in range(len(config.items(dsect))):
         for pdate in config.items(dsect)[i][1].split(','):
            if check_date(pdate,adate):
                if adv_debug:
                    print "check_date_Section(): found"
                return True
    return False


# @brief check if actual time fits user program time interval
        for user in userlist: # search in all user-specified sendq's
#
+              # neobiker: skip none user sections
# @param ptime program time intervall
+              if not (config.has_option(user,'voice_numbers') or config.has_option(user,'fax_numbers')):
# @param atime actual time
+                      continue
                userdata=pwd.getpwnam(user)
                ...
</pre>


def prog_time (ptime,atime):
=== File: ''/usr/bin/tbident.sh'' ===
    """prog_time() check if actual time fits user program time interval"""
Das Skript '''tbident.sh''' sucht mit '''lynx''' (muss installiert sein!) eine Telefonnummer online unter ''www.dasoertliche.de'' und cached jede Suche unter ''/var/cache/tbident/phonebook'':
    if ptime == '*':
        return True
    # From: time intervall start
    f0=ptime.split('-')[0] + ':00'
    f=[int(f0.split(':')[0]), int(f0.split(':')[1])]
    # To: time intervall end
    t0=ptime.split('-')[1] + ':00'
    t=[int(t0.split(':')[0]), int(t0.split(':')[1])]
    # actual time is between From - To Intervall?
    if adv_debug:
            print "Time test: ", f, " <= ", atime, " <= ", t, " ?"
    # 1.st test: f < t (means: t < 23:59)
    # 2.nd test: f > t (means: t >= 00:00 -> on next day!)
    if f < t:
        return f <= atime <= t
    else:
        return not (t <= atime <= f)


# @brief check if weekday is listed in user program
File ''/usr/bin/'''''tbident.sh''':
<pre>#!/bin/sh
# tbident.sh
#
#
# @param pwday program weekday
# Rückwärtssuche Telefonnummer -> Eintrag im Telefonbuch
# @param wday  actual weekday
 
def prog_wday (pwday,wday):
    """prog_wday() check if weekday is listed in user program"""
    if pwday == '*':
        return True
    wd={'MO': 0, 'DI': 1, 'MI': 2, 'DO': 3, 'FR': 4, 'SA': 5, 'SO': 6, \
                'TU': 1, 'WE': 2, 'TH': 3,                  'SU': 6}
    return wday==wd[pwday.upper()]
 
# @brief check if call_from is listed in callers section
#
#
# @param callers list of numbers or sections
# Usage: tbident.sh "09123 34 45 67"
 
# Ursprung: altes tbident.sh des VDR-Portal's
def check_caller_section (csect):
    """check_caller_section () check if call_from is listed in callers section"""
    for i in range(len(config.items(csect))):
        for j in config.items(csect)[i][1].split(','):
            if call_from == re.sub('[\s+()-]','',j):
                return True
    return False
 
# @brief check if call_from is listed in callers list/section
#
#
# @param callers list of numbers or sections
# Gefundene Telefonnummern werden gecached unter:
 
# >>> /var/cache/tbident/phonebook <<<
def check_caller (callers):
    """prog_caller () check if caller_from matches caller entries/section"""
    if callers == '*':
        return True
    for i in range(len(callers.split(','))):
        pcaller=re.sub('[\s+()-]','',callers.split(',')[i])
        if pcaller in config.sections():
            if check_caller_section(pcaller):
                return True
        elif call_from == pcaller:
                return True
    return False
 
# @brief check if given user program is active
#
#
# Check if Date/Time/Weekday fits actual date/time
# $Log: tbident.sh,v $
# to see if user program is activ
# Revision 1.4  2009/07/05 11:33:27  root
#
# added autom. updates of (grep) search parameters
# @param dates program dates
# @param times program time intervals
# @param wdays program week days
# @param d actual date/time
 
def prog_active (dates, times, wdays, d):
    """prog_active() check if given user program is active"""
    #
    # check the dates in user program
    found=False
    for i in range(len(dates.split(','))):
        pdate=dates.split(',')[i]
        if pdate in config.sections():
            if check_date_section(pdate,[d.day, d.month, d.year]):
                found=True
                break
        elif check_date(pdate,[d.day, d.month, d.year]):
            found=True
            break
    # date didn't fit
    if not found: return False
    #
    # check the times in user program
    found=False
    for i in range(len(times.split(','))):
        ptime=times.split(',')[i]
        if prog_time(ptime,[d.hour, d.minute]):
            found=True
            break
    # time intervall didn't fit
    if not found: return False
    #
    # check the weekdays in user program
    found=False
    for i in range(len(wdays.split(','))):
        pwday=wdays.split(',')[i]
        if prog_wday(pwday,d.weekday()):
            return True
    #
    # weekdays didn't fit
    return False
 
# @brief read user specific program for vbox
# --- prog[dates, times, weekdays, message, delay, length, callers] ---
#
#
# It will search for a valid user program (prog#) in the user section
# Revision 1.3  2008/06/05 11:35:00  root
# and reads corresponding definitions for
# *** empty log message ***
# - delay    the delay of vbox activation
# - message  the specific message to play
#
#
# @param (user-)section in config file to read
# Revision 1.2  2008/06/05 11:33:27  root
# @return True/False if valid programm found or not
# updated html syntax
# @user_prog['delay': xx, 'message': yy]
 
def read_prog (section):
    """read_prog(section) read user specific program for vbox"""
    prog="prog1"
    d=datetime.datetime.now()
    i=1
    while config.has_option(section,prog):
        prog="prog"+str(i)
        # p[dates, times, weekdays, message, delay, length, callers]
        p=config.get(section,prog).split()
        # check program dates/times/weekdays
        if prog_active(p[0], p[1], p[2] ,d):
            if adv_debug:
                print "prog active: "+prog+" ("+section+")"
            if check_caller(re.sub('[\s+()-]','',p[6])):
                user_prog['user']=section
                user_prog['prog']=i
                user_prog['dates']=p[0]
                user_prog['times']=p[1]
                user_prog['wdays']=p[2]
                user_prog['message']=p[3]
                user_prog['delay']=p[4]
                user_prog['length']=p[5]
                user_prog['callers']=p[6]
                if adv_debug:
                    print "checked: caller("+call_from+") in ", p[6]
                    print user_prog
                return True
        i=i+1
    return False
 
# @brief main function called by CapiSuite when an incoming call is received
#
#
# It will decide if this call should be accepted, with which service and for
# Revision 1.1  2007/09/09 15:50:43  root
# which user. The real call handling is done in faxIncoming and voiceIncoming.
# Initial revision
#
#
# @param call reference to the call. Needed by all capisuite functions
# @param service one of SERVICE_FAXG3, SERVICE_VOICE, SERVICE_OTHER
# @param call_from string containing the number of the calling party
# @param call_to string containing the number of the called party


def callIncoming(call,service,call_from,call_to):
set -e
        # read sections in config file
        try:
                config=cs_helpers.readConfig()
                userlist=config.sections()
                userlist.remove('GLOBAL')
                user_prog={}
                # debug messages for adv. features
                adv_debug=False
                # search for call_to in the user sections
                curr_user=""
                for u in userlist:
                        if config.has_option(u,'voice_numbers'):
                                numbers=config.get(u,'voice_numbers')
                                if (call_to in numbers.split(',') or numbers=="*"):
                                        if (service==capisuite.SERVICE_VOICE):
                                                curr_user=u
                                                curr_service=capisuite.SERVICE_VOICE
                                                break
                                        if (service==capisuite.SERVICE_FAXG3):
                                                curr_user=u
                                                curr_service=capisuite.SERVICE_FAXG3
                                                break


                        if config.has_option(u,'fax_numbers'):
NR="$@"
                                numbers=config.get(u,'fax_numbers')
NUMMER=`echo "$NR" | tr -d ' ()' | sed -e "s/^\+49//"`
                                if (call_to in numbers.split(',') or numbers=="*"):
NAME=""
                                        if (service in (capisuite.SERVICE_FAXG3,capisuite.SERVICE_VOICE)):
DETAILS=""
                                                curr_user=u
                                                curr_service=capisuite.SERVICE_FAXG3
                                                break


        except IOError,e:
URL="http://www.dasoertliche.de/?form_name=search_inv&action=1&&ph="
                capisuite.error("Error occured during config file reading: "+e+" Disconnecting...")
CONTEXT='Trefferliste'
                capisuite.reject(call,0x34A9)
                return
        # answer the call with the right service
        if (curr_user==""):
                capisuite.log("call from "+call_from+" to "+call_to+" ignoring",1,call)
                capisuite.reject(call,1)
                return
        try:
                if (curr_service==capisuite.SERVICE_VOICE):
                        delay=cs_helpers.getOption(config,curr_user,"voice_delay")
                        active_user_prog=read_prog(curr_user)
                        if active_user_prog:
                            delay=user_prog['delay']
                        if (delay==None):
                                capisuite.error("voice_delay not found for user "+curr_user+"! -> rejecting call")
                                capisuite.reject(call,0x34A9)
                                return
                        capisuite.log("call from "+call_from+" to "+call_to+" for "+curr_user+" connecting with voice",1,call)
                        capisuite.connect_voice(call,int(delay))
                        voiceIncoming(call,call_from,call_to,curr_user,config)
                elif (curr_service==capisuite.SERVICE_FAXG3):
                        faxIncoming(call,call_from,call_to,curr_user,config,0)
        except capisuite.CallGoneError: # catch exceptions from connect_*
                (cause,causeB3)=capisuite.disconnect(call)
                capisuite.log("connection lost with cause 0x%x,0x%x" % (cause,causeB3),1,call)


# @brief called by callIncoming when an incoming fax call is received
CACHE=/var/cache/tbident/phonebook
#
TMPFILE=/tmp/tbident_$$
# @param call reference to the call. Needed by all capisuite functions
TMPVARS=/var/cache/tbident/tbident.vars
# @param call_from string containing the number of the calling party
TIMEOUT=3
# @param call_to string containing the number of the called party
# @param curr_user name of the user who is responsible for this
# @param config ConfigParser instance holding the config data
# @param already_connected 1 if we're already connected (that means we must switch to fax mode)
def faxIncoming(call,call_from,call_to,curr_user,config,already_connected):
        try:
                udir=cs_helpers.getOption(config,"","fax_user_dir")
                if (udir==None):
                        capisuite.error("global option fax_user_dir not found! -> rejecting call")
                        capisuite.reject(call,0x34A9)
                        return
                udir=os.path.join(udir,curr_user)+"/"
                if (not os.access(udir,os.F_OK)):
                        userdata=pwd.getpwnam(curr_user)
                        os.mkdir(udir,0700)
                        os.chown(udir,userdata[2],userdata[3])
                if (not os.access(udir+"received/",os.F_OK)):
                        userdata=pwd.getpwnam(curr_user)
                        os.mkdir(udir+"received/",0700)
                        os.chown(udir+"received/",userdata[2],userdata[3])
        except KeyError:
                capisuite.error("user "+curr_user+" is not a valid system user. Disconnecting",call)
                capisuite.reject(call,0x34A9)
                return
        filename="" # assure the variable is defined...
        try:
                stationID=cs_helpers.getOption(config,curr_user,"fax_stationID")
                if (stationID==None):
                        capisuite.error("Warning: fax_stationID not found for user "+curr_user+" -> using empty string")
                        stationID=""
                headline=cs_helpers.getOption(config,curr_user,"fax_headline","") # empty string is no problem here
                capisuite.log("call from "+call_from+" to "+call_to+" for "+curr_user+" connecting with fax",1,call)
                if (already_connected):
                        faxInfo=capisuite.switch_to_faxG3(call,stationID,headline)
                else:
                        faxInfo=capisuite.connect_faxG3(call,stationID,headline,0)
                if (faxInfo!=None and faxInfo[3]==1):
                        faxFormat="cff" # color fax
                else:
                        faxFormat="sff" # normal b&w fax
                filename=cs_helpers.uniqueName(udir+"received/","fax",faxFormat)
                capisuite.fax_receive(call,filename)
                (cause,causeB3)=capisuite.disconnect(call)
                capisuite.log("connection finished with cause 0x%x,0x%x" % (cause,causeB3),1,call)


        except capisuite.CallGoneError: # catch this here to get the cause info in the mail
if [ ! -x /usr/bin/lynx ]; then
                (cause,causeB3)=capisuite.disconnect(call)
  echo "Error: Lynx not installed."
                capisuite.log("connection lost with cause 0x%x,0x%x" % (cause,causeB3),1,call)
  exit 1
fi


        if (os.access(filename,os.R_OK)):
if [ ! -e /usr/bin/tbident-check.sh ]; then
                cs_helpers.writeDescription(filename,
  echo "Error: tbident-check.sh not installed."
                  "call_from=\""+call_from+"\"\ncall_to=\""+call_to+"\"\ntime=\""
  exit 1
                  +time.ctime()+"\"\ncause=\"0x%x/0x%x\"\n" % (cause,causeB3))
fi
                userdata=pwd.getpwnam(curr_user)
                os.chmod(filename,0600)
                os.chown(filename,userdata[2],userdata[3])
                os.chmod(filename[:-3]+"txt",0600)
                os.chown(filename[:-3]+"txt",userdata[2],userdata[3])


                fromaddress=cs_helpers.getOption(config,curr_user,"fax_email_from","")
if [ ! -e $CACHE ]; then
                if (fromaddress==""):
    mkdir -p `dirname $CACHE`
                        fromaddress=curr_user
    touch $CACHE
                mailaddress=cs_helpers.getOption(config,curr_user,"fax_email","")
fi
                if (mailaddress==""):
grep "^${NUMMER}|" $CACHE && exit 0
                        mailaddress=curr_user
                action=cs_helpers.getOption(config,curr_user,"fax_action","").lower()
                if (action not in ("mailandsave","saveonly")):
                        capisuite.error("Warning: No valid fax_action definition found for user "+curr_user+" -> assuming SaveOnly")
                        action="saveonly"
                if (action=="mailandsave"):
                        cs_helpers.sendMIMEMail(fromaddress, mailaddress, "Fax received from "+call_from+" to "+call_to, faxFormat,
                          "You got a fax from "+call_from+" to "+call_to+"\nDate: "+time.ctime()+"\n\n"
                          +"See attached file.\nThe original file was saved to file://"+filename+"\n\n", filename)


# @brief called by callIncoming when an incoming voice call is received
# read variables (daily per cron updated)
#
if [ ! -e "$TMPVARS" ]; then
# @param call reference to the call. Needed by all capisuite functions
    tbident-check.sh
# @param call_from string containing the number of the calling party
fi
# @param call_to string containing the number of the called party
. $TMPVARS
# @param curr_user name of the user who is responsible for this
# @param config ConfigParser instance holding the config data
def voiceIncoming(call,call_from,call_to,curr_user,config):
        try:
                udir=cs_helpers.getOption(config,"","voice_user_dir")
                if (udir==None):
                        capisuite.error("global option voice_user_dir not found! -> rejecting call")
                        capisuite.reject(call,0x34A9)
                        return
                udir=os.path.join(udir,curr_user)+"/"
                if (not os.access(udir,os.F_OK)):
                        userdata=pwd.getpwnam(curr_user)
                        os.mkdir(udir,0700)
                        os.chown(udir,userdata[2],userdata[3])
                if (not os.access(udir+"received/",os.F_OK)):
                        userdata=pwd.getpwnam(curr_user)
                        os.mkdir(udir+"received/",0700)
                        os.chown(udir+"received/",userdata[2],userdata[3])
        except KeyError:
                capisuite.error("user "+curr_user+" is not a valid system user. Disconnecting",call)
                capisuite.reject(call,0x34A9)
                return
        filename=cs_helpers.uniqueName(udir+"received/","voice","la")
        action=cs_helpers.getOption(config,curr_user,"voice_action","").lower()
        if (action not in ("mailandsave","saveonly","none")):
                capisuite.error("Warning: No valid voice_action definition found for user "+curr_user+" -> assuming SaveOnly")
                action="saveonly"
        try:
                capisuite.enable_DTMF(call)
                if not active_user_prog:
                    userannouncement=udir+cs_helpers.getOption(config,curr_user,"announcement","announcement.la")
                else:
                    userannouncement=udir+user_prog['message']
                pin=cs_helpers.getOption(config,curr_user,"pin","")
                if (os.access(userannouncement,os.R_OK)):
                        capisuite.audio_send(call,userannouncement,1)
                else:
                        if (call_to!="-"):
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"anrufbeantworter-von.la"),1)
                                cs_helpers.sayNumber(call,call_to,curr_user,config)
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"bitte-nachricht.la"),1)


                if (action!="none"):
# get 1.st match (-m1) of list
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"beep.la"),1)
GREP="grep $CONTEXT $GREP_OPT -m1 $TMPFILE"
                        length=cs_helpers.getOption(config,curr_user,"record_length","60")
                        silence_timeout=cs_helpers.getOption(config,curr_user,"record_silence_timeout","5")
                        capisuite.audio_receive(call,filename,int(length), int(silence_timeout),1)


                dtmf_list=capisuite.read_DTMF(call,0)
lynx "${URL}${NUMMER}" -dump -nolist -connect_timeout=$TIMEOUT >$TMPFILE 2>/dev/null
                if (dtmf_list=="X"):
                        if (os.access(filename,os.R_OK)):
                                os.unlink(filename)
                        faxIncoming(call,call_from,call_to,curr_user,config,1)
                elif (dtmf_list!="" and pin!=""):
                        dtmf_list+=capisuite.read_DTMF(call,3) # wait 5 seconds for input
                        count=1
                        while (count<3 and pin!=dtmf_list):  # try again if input was wrong
                                capisuite.log("wrong PIN entered...",1,call)
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"beep.la"))
                                dtmf_list=capisuite.read_DTMF(call,3)
                                count+=1
                        if (pin==dtmf_list):
                                if (os.access(filename,os.R_OK)):
                                        os.unlink(filename)
                                capisuite.log("Starting remote inquiry...",1,call)
                                remoteInquiry(call,udir,curr_user,config)


                (cause,causeB3)=capisuite.disconnect(call)
NAME=`$GREP | awk "{ if (NR == $NAME_LINE) print }" | sed -e "s/$NAME_FILTER\$//g" |
                capisuite.log("connection finished with cause 0x%x,0x%x" % (cause,causeB3),1,call)
              sed -e 's/^ *//' -e 's/ *$//'`
DETAILS=`$GREP | awk "{ if (NR == $ADRS_LINE) print }" | sed -e "s/$ADRS_FILTER\$//g" |
              sed -e 's/^ *//' -e 's/ *$//'`


        except capisuite.CallGoneError: # catch this here to get the cause info in the mail
if [ -n "$NAME" ]; then
                (cause,causeB3)=capisuite.disconnect(call)
    echo "$NUMMER|$NAME|$DETAILS"
                capisuite.log("connection lost with cause 0x%x,0x%x" % (cause,causeB3),1,call)
    echo "$NUMMER|$NAME|$DETAILS" >> $CACHE
else
    echo "$NR|Unbekannt|"
    echo "$NR|Unbekannt|" >> $CACHE
fi


        if (os.access(filename,os.R_OK)):
rm $TMPFILE
                cs_helpers.writeDescription(filename,
</pre>
                  "call_from=\""+call_from+"\"\ncall_to=\""+call_to+"\"\ntime=\""
File ''/usr/bin/'''''tbident-check.sh''':
                  +time.ctime()+"\"\ncause=\"0x%x/0x%x\"\n" % (cause,causeB3))
<pre>#!/bin/sh
                userdata=pwd.getpwnam(curr_user)
# tbident-check.sh
                os.chmod(filename,0600)
                os.chown(filename,userdata[2],userdata[3])
                os.chmod(filename[:-2]+"txt",0600)
                os.chown(filename[:-2]+"txt",userdata[2],userdata[3])
 
                fromaddress=cs_helpers.getOption(config,curr_user,"voice_email_from","")
                if (fromaddress==""):
                        fromaddress=curr_user
                mailaddress=cs_helpers.getOption(config,curr_user,"voice_email","")
                if (mailaddress==""):
                        mailaddress=curr_user
                if (action=="mailandsave"):
                        cs_helpers.sendMIMEMail(fromaddress, mailaddress, "Voice call received from "+call_from+" to "+call_to, "la",
                          "You got a voice call from "+call_from+" to "+call_to+"\nDate: "+time.ctime()+"\n\n"
                          +"See attached file.\nThe original file was saved to file://"+filename+"\n\n", filename)
 
 
# @brief remote inquiry function (uses german wave snippets!)
#
#
# commands for remote inquiry
# Aktuelle Syntax prüfen und Variablen autom. setzen
# delete message - 1
# Rückwärtssuche Telefonnummer -> Eintrag im Telefonbuch
# next message - 4
# last message - 5
# repeat current message - 6
#
#
# @param call reference to the call. Needed by all capisuite functions
# @param userdir spool_dir of the current_user
# @param curr_user name of the user who is responsible for this
# @param config ConfigParser instance holding the config data
def remoteInquiry(call,userdir,curr_user,config):
        import time,fcntl,errno,os
        # acquire lock
        lockfile=open(userdir+"received/inquiry_lock","w")
        try:
                fcntl.lockf(lockfile,fcntl.LOCK_EX | fcntl.LOCK_NB) # only one inquiry at a time!


        except IOError,err: # can't get the lock
NR="091329040"
                if (err.errno in (errno.EACCES,errno.EAGAIN)):
NUMMER=`echo "$NR" | tr -d ' ()' | sed -e "s/^\+49//"`
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"fernabfrage-aktiv.la"))
                        lockfile.close()
                        return


        try:
URL="http://www.dasoertliche.de/?form_name=search_inv&action=1&&ph="
                # read directory contents
CONTEXT='Trefferliste'
                messages=os.listdir(userdir+"received/")
NAME=""
                messages=filter (lambda s: re.match("voice-.*\.la",s),messages)  # only use voice-* files
DETAILS=""
                messages=map(lambda s: int(re.match("voice-([0-9]+)\.la",s).group(1)),messages) # filter out numbers
                messages.sort()


                # read the number of the message heard last at the last inquiry
TMPFILE=/tmp/tbident_$$
                lastinquiry=-1
TMPVARS=/var/cache/tbident/tbident.vars
                if (os.access(userdir+"received/last_inquiry",os.W_OK)):
TIMEOUT=3
                        lastfile=open(userdir+"received/last_inquiry","r")
                        lastinquiry=int(lastfile.readline())
                        lastfile.close()


                # sort out old messages
if [ ! -x /usr/bin/lynx ]; then
                oldmessages=[]
  echo "Error: Lynx not installed."
                i=0
  exit 1
                while (i<len(messages)):
fi
                        if (messages[i]<=lastinquiry):
                                oldmessages.append(messages[i])
                                del messages[i]
                        else:
                                i+=1


                cs_helpers.sayNumber(call,str(len(messages)),curr_user,config)
if [ ! -e "$TMPVARS" ]; then
                if (len(messages)==1):
    cat > $TMPVARS << EOT
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"neue-nachricht.la"),1)
NAME_LINE=0
                else:
ADRS_LINE=0
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"neue-nachrichten.la"),1)
NAME_FILTER=
ADRS_FILTER=
GREP_OPT=
EOT
    chmod a+rx $TMPVARS
fi
mv $TMPVARS ${TMPVARS}.bak


                # menu for record new announcement
lynx "${URL}${NUMMER}" -dump -nolist -connect_timeout=$TIMEOUT -dont_wrap_pre >$TMPFILE 2>/dev/null ||
                cmd=""
    { echo "Error $0: Lynx failed."
                while (cmd not in ("1","9")):
      exit 1
                        if (len(messages)+len(oldmessages)):
    }
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"zum-abhoeren-1.la"),1)
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"fuer-neue-ansage-9.la"),1)
                        cmd=capisuite.read_DTMF(call,0,1)
                if (cmd=="9"):
                        newAnnouncement(call,userdir,curr_user,config)
                        return


                # start inquiry
awk '
                for curr_msgs in (messages,oldmessages):
    BEGIN {
                        cs_helpers.sayNumber(call,str(len(curr_msgs)),curr_user,config)
      srch="Trefferliste"
                        if (curr_msgs==messages):
      nam="Herzo Werke GmbH"
                                if (len(curr_msgs)==1):
      adr="Schießhausstr. 9, 91074 Herzogenaurach"
                                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"neue-nachricht.la"),1)
      srch_line=0
                                else:
      nam_line=0
                                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"neue-nachrichten.la"),1)
      adr_line=0
                        else:
    }
                                if (len(curr_msgs)==1):
    /Trefferliste/ { srch_line = NR }
                                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"nachricht.la"),1)
                                else:
                                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"nachrichten.la"),1)


                        i=0
    /Herzo Werke GmbH/ {
                        while (i<len(curr_msgs)):
      nam_line = NR
                                filename=userdir+"received/voice-"+str(curr_msgs[i])+".la"
      drop_nam = index($0, nam) + length(nam)
                                descr=cs_helpers.readConfig(filename[:-2]+"txt")
      drop_nam_str = substr($0, drop_nam)
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"nachricht.la"),1)
      }
                                cs_helpers.sayNumber(call,str(i+1),curr_user,config)
    /Schießhausstr. 9, 91074 Herzogenaurach/ {
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"von.la"),1)
      adr_line = NR
                                cs_helpers.sayNumber(call,descr.get('GLOBAL','call_from'),curr_user,config)
      drop_adr = index($0, adr) + length(adr)
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"fuer.la"),1)
      drop_adr_str = substr($0, drop_adr)
                                cs_helpers.sayNumber(call,descr.get('GLOBAL','call_to'),curr_user,config)
      }
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"am.la"),1)
    END {
                                calltime=time.strptime(descr.get('GLOBAL','time'))
      print "GREP_OPT=\"-A" max(adr_line, nam_line) - srch_line "\""
                                cs_helpers.sayNumber(call,str(calltime[2]),curr_user,config)
      print "NAME_LINE=\"" nam_line - srch_line + 1 "\""
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"..la"),1)
      print "ADRS_LINE=\"" adr_line - srch_line + 1 "\""
                                cs_helpers.sayNumber(call,str(calltime[1]),curr_user,config)
      if ( length(drop_nam_str) ) printf "NAME_FILTER=\"%s\"\n", drop_nam_str
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"..la"),1)
      else print "NAME_FILTER="
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"um.la"),1)
      if ( length(drop_adr_str) ) printf "ADRS_FILTER=\"%s\"\n", drop_adr_str
                                cs_helpers.sayNumber(call,str(calltime[3]),curr_user,config)
      else print "ADRS_FILTER="
                                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"uhr.la"),1)
    }
                                cs_helpers.sayNumber(call,str(calltime[4]),curr_user,config)
    function max(a,b)
                                capisuite.audio_send(call,filename,1)
    {
                                cmd=""
      if (a<b) return b
                                while (cmd not in ("1","4","5","6")):
      else return a
                                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"erklaerung.la"),1)
    }
                                        cmd=capisuite.read_DTMF(call,0,1)
' $TMPFILE > $TMPVARS ||
                                if (cmd=="1"):
      { echo "Error $0: Awk failed."
                                        os.remove(filename)
        mv ${TMPVARS}.bak $TMPVARS
                                        os.remove(filename[:-2]+"txt")
        exit 1
                                        del curr_msgs[i]
      }
                                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"nachricht-geloescht.la"))
                                elif (cmd=="4"):
                                        if (curr_msgs[i]>lastinquiry):
                                                lastinquiry=curr_msgs[i]
                                                lastfile=open(userdir+"received/last_inquiry","w")
                                                lastfile.write(str(curr_msgs[i])+"\n")
                                                lastfile.close()
                                        i+=1
                                elif (cmd=="5"):
                                        i-=1
                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"keine-weiteren-nachrichten.la"))


        finally:
chmod a+rx $TMPVARS
                # unlock
                fcntl.lockf(lockfile,fcntl.LOCK_UN)
                lockfile.close()
                os.unlink(userdir+"received/inquiry_lock")


# @brief remote inquiry: record new announcement (uses german wave snippets!)
diff $TMPVARS ${TMPVARS}.bak >/dev/null || echo "Info $0: Vars updated"
#
rm $TMPFILE
# @param call reference to the call. Needed by all capisuite functions
# @param userdir spool_dir of the current_user
# @param curr_user name of the user who is responsible for this
# @param config ConfigParser instance holding the config data
def newAnnouncement(call,userdir,curr_user,config):
        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"bitte-neue-ansage-komplett.la"))
        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"beep.la"))
        cmd=""
        while (cmd!="1"):
                capisuite.audio_receive(call,userdir+"announcement-tmp.la",60,3)
                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"neue-ansage-lautet.la"))
                capisuite.audio_send(call,userdir+"announcement-tmp.la")
                capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"wenn-einverstanden-1.la"))
                cmd=capisuite.read_DTMF(call,0,1)
                if (cmd!="1"):
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"bitte-neue-ansage-kurz.la"))
                        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"beep.la"))
        userannouncement=userdir+cs_helpers.getOption(config,curr_user,"announcement","announcement.la")
        os.rename(userdir+"announcement-tmp.la",userannouncement)
        userdata=pwd.getpwnam(curr_user)
        os.chown(userannouncement,userdata[2],userdata[3])
 
        capisuite.audio_send(call,cs_helpers.getAudio(config,curr_user,"ansage-gespeichert.la"))
 
#
# History:
#
# $Log: incoming.py,v $
# Revision 1.9.2.1  2003/08/24 12:47:19  gernot
# - faxIncoming tried to reconnect when it was called after a switch from
#  voice to fax mode, which lead to a call abort. Thx to Harald Jansen &
#  Andreas Scholz for reporting!
#
# Revision 1.9  2003/06/27 07:51:09  gernot
# - replaced german umlaut in filename "nachricht-gelscht.la", can cause
#  problems on Redhat, thx to Herbert Hübner for reporting
#
# Revision 1.8  2003/06/16 10:21:05  gernot
# - define filename in any case (thx to Axel Schneck for reporting and
#  analyzing...)
#
# Revision 1.7  2003/05/25 13:38:30  gernot
# - support reception of color fax documents
#
# Revision 1.6  2003/04/10 21:29:51  gernot
# - support empty destination number for incoming calls correctly (austrian
#  telecom does this (sic))
# - core now returns "-" instead of "??" for "no number available" (much nicer
#  in my eyes)
# - new wave file used in remote inquiry for "unknown number"
#
# Revision 1.5  2003/03/20 09:12:42  gernot
# - error checking for reading of configuration improved, many options got
#  optional, others produce senseful error messages now if not found,
#  fixes bug# 531, thx to Dieter Pelzel for reporting
#
# Revision 1.4  2003/03/13 11:08:06  gernot
# - fix remote inquiry locking (should fix bug #534, but doesn't - anyway,
#  this fix is definitely necessary)
# - stricter permissions of saved files and created dirs, fixes #544
# - add "file://" prefix to the path shown in the mails to the user
#
# Revision 1.3  2003/02/21 13:13:34  gernot
# - removed some debug output (oops...)
#
# Revision 1.2  2003/02/21 11:02:17  gernot
# - removed os.setuid() from incoming script
#  -> fixes Bug #527
#
# Revision 1.1.1.1  2003/02/19 08:19:54  gernot
# initial checkin of 0.4
#
# Revision 1.11  2003/02/17 11:13:43  ghillie
# - remoteinquiry supports new and old messages now
#
# Revision 1.10  2003/02/03 14:50:08  ghillie
# - fixed small typo
#
# Revision 1.9  2003/01/31 16:32:41  ghillie
# - support "*" for "all numbers"
# - automatic switch voice->fax when SI says fax
#
# Revision 1.8  2003/01/31 11:24:41  ghillie
# - wrong user handling for more than one users fixed
# - creates user_dir/user and user_dir/user/received now separately as
#  idle.py can also create user_dir/user now
#
# Revision 1.7  2003/01/27 21:57:54  ghillie
# - fax_numbers and voice_numbers may not exist (no fatal error any more)
# - accept missing email option
# - fixed typo
#
# Revision 1.6  2003/01/27 19:24:29  ghillie
# - updated to use new configuration files for fax & answering machine
#
# Revision 1.5  2003/01/19 12:03:27  ghillie
# - use capisuite log functions instead of stdout/stderr
#
# Revision 1.4  2003/01/17 15:09:49  ghillie
# - cs_helpers.sendMail was renamed to sendMIMEMail
#
# Revision 1.3  2003/01/16 12:58:34  ghillie
# - changed DTMF timeout for pin to 3 seconds
# - delete recorded wave if fax or remote inquiry is recognized
# - updates in remoteInquiry: added menu for recording own announcement
# - fixed some typos
# - remoteInquiry: delete description file together with call if requested
# - new function: newAnnouncement
#
# Revision 1.2  2003/01/15 15:55:12  ghillie
# - added exception handler in callIncoming
# - faxIncoming: small typo corrected
# - voiceIncoming & remoteInquiry: updated to new config file system
#
# Revision 1.1  2003/01/13 16:12:58  ghillie
# - renamed from incoming.pyin to incoming.py as all previously processed
#  variables are moved to config and cs_helpers.pyin
#
# Revision 1.4  2002/12/18 14:34:56  ghillie
# - added some informational prints
# - accept voice calls to fax nr
#
# Revision 1.3  2002/12/16 15:04:51  ghillie
# - added missing path prefix to delete routing in remote inquiry
#
# Revision 1.2  2002/12/16 13:09:25  ghillie
# - added some comments about the conf_* vars
# - added conf_wavedir
# - added support for B3 cause now returned by disconnect()
# - corrected some dir entries to work in installed system
#
# Revision 1.1  2002/12/14 13:53:18  ghillie
# - idle.py and incoming.py are now auto-created from *.pyin
#
# Revision 1.4  2002/12/11 12:58:05  ghillie
# - read return value from disconnect()
# - added disconnect() to exception handler
#
# Revision 1.3  2002/12/09 15:18:35  ghillie
# - added disconnect() in exception handler
#
# Revision 1.2  2002/12/02 21:30:42  ghillie
# fixed some minor typos
#
# Revision 1.1  2002/12/02 21:15:55  ghillie
# - moved scripts to own directory
# - added remote-connect script to repository
#
# Revision 1.20  2002/12/02 20:59:44  ghillie
# another typo :-|
#
# Revision 1.19  2002/12/02 20:54:07  ghillie
# fixed small typo
#
# Revision 1.18  2002/12/02 16:51:32  ghillie
# nearly complete new script, supports answering machine, fax receiving and remote inquiry now
#
# Revision 1.17  2002/11/29 16:28:43  ghillie
# - updated syntax (connect_telephony -> connect_voice)
#
# Revision 1.16  2002/11/29 11:09:04  ghillie
# renamed CapiCom to CapiSuite (name conflict with MS crypto API :-( )
#
# Revision 1.15  2002/11/25 11:43:43  ghillie
# updated to new syntax
#
# Revision 1.14  2002/11/23 16:16:17  ghillie
# moved switch2fax after audio_receive()
#
# Revision 1.13  2002/11/22 15:48:58  ghillie
# renamed pcallcontrol module to capicom
#
# Revision 1.12  2002/11/22 15:02:39  ghillie
# - added automatic switch between speech and fax
# - some comments added
#
# Revision 1.11  2002/11/19 15:57:18  ghillie
# - Added missing throw() declarations
# - phew. Added error handling. All exceptions are caught now.
#
# Revision 1.10  2002/11/18 12:32:36  ghillie
# - callIncoming lives now in __main__, not necessarily in pcallcontrol any more
# - added some comments and header
#
</pre>
</pre>


File: ''/usr/lib/capisuite/idle_adv.py''
[[Neobiker%27s_Wiki:Portal|Zurück zum Portal]]
<pre>
#              idle_adv.py - default script for capisuite
#              ---------------------------------------------
#    copyright            : (C) 2007 neobiker
#
#    copyright            : (C) 2002 by Gernot Hillier
#    email                : gernot@hillier.de
#    version              : $Revision: 1.8.2.2 $
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
 
import os,re,time,pwd,fcntl
# capisuite stuff
import capisuite,cs_helpers
 
def idle(capi):
        config=cs_helpers.readConfig()
        spool=cs_helpers.getOption(config,"","spool_dir")
        if (spool==None):
                capisuite.error("global option spool_dir not found.")
                return
 
        done=os.path.join(spool,"done")+"/"
        failed=os.path.join(spool,"failed")+"/"
 
        if (not os.access(done,os.W_OK) or not os.access(failed,os.W_OK)):
                capisuite.error("Can't read/write to the necessary spool dirs")
                return
 
        userlist=config.sections()
        userlist.remove('GLOBAL')
 
        for user in userlist: # search in all user-specified sendq's
                # skip none user sections
                if not config.has_option(user,'voice_numbers'):
                        break
                userdata=pwd.getpwnam(user)
                outgoing_nr=cs_helpers.getOption(config,user,"outgoing_MSN","")
                if (outgoing_nr==""):
                        incoming_nrs=cs_helpers.getOption(config,user,"fax_numbers","")
                        if (incoming_nrs==""):
                                continue
                        else:
                                outgoing_nr=(incoming_nrs.split(','))[0]
 
                udir=cs_helpers.getOption(config,"","fax_user_dir")
                if (udir==None):
                        capisuite.error("global option fax_user_dir not found.")
                        return
                udir=os.path.join(udir,user)+"/"
                sendq=os.path.join(udir,"sendq")+"/"
                if (not os.access(udir,os.F_OK)):
                        os.mkdir(udir,0700)
                        os.chown(udir,userdata[2],userdata[3])
                if (not os.access(sendq,os.F_OK)):
                        os.mkdir(sendq,0700)
                        os.chown(sendq,userdata[2],userdata[3])
 
                files=os.listdir(sendq)
                files=filter (lambda s: re.match("fax-.*\.txt",s),files)
 
                for job in files:
                        job_fax=job[:-3]+"sff"
                        real_user_c=os.stat(sendq+job).st_uid
                        real_user_j=os.stat(sendq+job_fax).st_uid
                        if (real_user_j!=pwd.getpwnam(user)[2] or real_user_c!=pwd.getpwnam(user)[2]):
                                capisuite.error("job "+sendq+job_fax+" seems to be manipulated (wrong uid)! Ignoring...")
                                continue
 
                        lockfile=open(sendq+job[:-3]+"lock","w")
                        # read directory contents
                        fcntl.lockf(lockfile,fcntl.LOCK_EX) # lock so that it isn't deleted while sending
 
                        if (not os.access(sendq+job,os.W_OK)): # perhaps it was cancelled?
                                fcntl.lockf(lockfile,fcntl.LOCK_UN)
                                lockfile.close()
                                os.unlink(sendq+job[:-3]+"lock")
                                continue
 
                        control=cs_helpers.readConfig(sendq+job)
                        # set DST value to -1 (unknown), as strptime sets it wrong for some reason
                        starttime=(time.strptime(control.get("GLOBAL","starttime")))[0:8]+(-1,)
                        starttime=time.mktime(starttime)
                        if (starttime>time.time()):
                                fcntl.lockf(lockfile,fcntl.LOCK_UN)
                                lockfile.close()
                                os.unlink(sendq+job[:-3]+"lock")
                                continue
 
                        tries=control.getint("GLOBAL","tries")
                        dialstring=control.get("GLOBAL","dialstring")
                        addressee=cs_helpers.getOption(control,"GLOBAL","addressee","")
                        subject=cs_helpers.getOption(control,"GLOBAL","subject","")
                        mailaddress=cs_helpers.getOption(config,user,"fax_email","")
                        if (mailaddress==""):
                                mailaddress=user
                        fromaddress=cs_helpers.getOption(config,user,"fax_email_from","")
                        if (fromaddress==""):
                                fromaddress=user
 
                        capisuite.log("job "+job_fax+" from "+user+" to "+dialstring+" initiated",1)
                        result,resultB3 = sendfax(capi,sendq+job_fax,outgoing_nr,dialstring,user,config)
                        tries+=1
                        capisuite.log("job "+job_fax+": result was %x,%x" % (result,resultB3),1)
 
                        if (result in (0,0x3400,0x3480,0x3490,0x349f) and resultB3==0):
                                movejob(job_fax,sendq,done,user)
                                capisuite.log("job "+job_fax+": finished successfully",1)
                                mailtext="Your fax job to "+addressee+" ("+dialstring+") was sent successfully.\n\n" \
                                  +"Subject: "+subject+"\nFilename: "+job_fax \
                                  +"\nNeeded tries: "+str(tries) \
                                  +("\nLast result: 0x%x/0x%x" % (result,resultB3)) \
                                  +"\n\nIt was moved to file://"+done+user+"-"+job_fax
                                cs_helpers.sendSimpleMail(fromaddress,mailaddress,
                                  "Fax to "+addressee+" ("+dialstring+") sent successfully.",
                                  mailtext)
                        else:
                                max_tries=int(cs_helpers.getOption(config,"","send_tries","10"))
                                delays=cs_helpers.getOption(config,"","send_delays","60,60,60,300,300,3600,3600,18000,36000").split(",")
                                delays=map(int,delays)
                                if ((tries-1)<len(delays)):
                                        next_delay=delays[tries-1]
                                else:
                                        next_delay=delays[-1]
                                starttime=time.time()+next_delay
                                capisuite.log("job "+job_fax+": delayed for "+str(next_delay)+" seconds",2)
                                cs_helpers.writeDescription(sendq+job_fax,"dialstring=\""+dialstring+"\"\n"
                                  +"starttime=\""+time.ctime(starttime)+"\"\ntries=\""+str(tries)+"\"\n"
                                  +"user=\""+user+"\"\naddressee=\""+addressee+"\"\nsubject=\""+subject+"\"\n")
                                if (tries>=max_tries):
                                        movejob(job_fax,sendq,failed,user)
                                        capisuite.log("job "+job_fax+": failed finally",1)
                                        mailtext="I'm sorry, but your fax job to "+addressee+" ("+dialstring \
                                          +") failed finally.\n\nSubject: "+subject \
                                          +"\nFilename: "+job_fax+"\nTries: "+str(tries) \
                                          +"\nLast result: 0x%x/0x%x" % (result,resultB3) \
                                          +"\n\nIt was moved to file://"+failed+user+"-"+job_fax
                                        cs_helpers.sendSimpleMail(fromaddress,mailaddress,
                                          "Fax to "+addressee+" ("+dialstring+") FAILED.",
                                          mailtext)
 
                        fcntl.lockf(lockfile,fcntl.LOCK_UN)
                        lockfile.close()
                        os.unlink(sendq+job[:-3]+"lock")
 
def sendfax(capi,job,outgoing_nr,dialstring,user,config):
        try:
                controller=int(cs_helpers.getOption(config,"","send_controller","1"))
                timeout=int(cs_helpers.getOption(config,user,"outgoing_timeout","60"))
                stationID=cs_helpers.getOption(config,user,"fax_stationID")
                if (stationID==None):
                        capisuite.error("Warning: fax_stationID for user "+user+" not set")
                        stationID=""
                headline=cs_helpers.getOption(config,user,"fax_headline","")
                (call,result)=capisuite.call_faxG3(capi,controller,outgoing_nr,dialstring,timeout,stationID,headline)
                if (result!=0):
                        return(result,0)
                capisuite.fax_send(call,job)
                return(capisuite.disconnect(call))
        except capisuite.CallGoneError:
                return(capisuite.disconnect(call))
 
def movejob(job,olddir,newdir,user):
        os.rename(olddir+job,newdir+user+"-"+job)
        os.rename(olddir+job[:-3]+"txt",newdir+user+"-"+job[:-3]+"txt")
 
#
# History:
#
# $Log: idle.py,v $
# Revision 1.8.2.2  2004/01/10 07:56:27  gernot
# - fax_numbers is really allowed to miss now (taken from MAIN, 1.11)...
#
# Revision 1.8.2.1  2003/09/21 12:35:20  gernot
# - add 0x349f to list of normal results
#
# Revision 1.8  2003/06/26 11:53:17  gernot
# - fax jobs can be given an addressee and a subject now (resolves #18, reported
#  by Achim Bohnet)
#
# Revision 1.7  2003/06/19 14:58:43  gernot
# - fax_numbers is now really optional (bug #23)
# - tries counter was wrongly reported (bug #29)
#
# Revision 1.6  2003/04/06 11:07:40  gernot
# - fix for 1-hour-delayed sending of fax (DST problem)
#
# Revision 1.5  2003/03/20 09:12:42  gernot
# - error checking for reading of configuration improved, many options got
#  optional, others produce senseful error messages now if not found,
#  fixes bug# 531, thx to Dieter Pelzel for reporting
#
# Revision 1.4  2003/03/13 11:09:58  gernot
# - use stricted permissions for saved files and created userdirs. Fixes
#  bug #544
#
# Revision 1.3  2003/03/09 11:48:10  gernot
# - removed wrong unlock operation (lock not acquired at this moment!)
#
# Revision 1.2  2003/03/06 09:59:11  gernot
# - added "file://" as prefix to filenames in sent mails, thx to
#  Achim Bohnet for this suggestion
#
# Revision 1.1.1.1  2003/02/19 08:19:54  gernot
# initial checkin of 0.4
#
# Revision 1.12  2003/02/18 09:54:22  ghillie
# - added missing lockfile deletions, corrected locking protocol
#  -> fixes Bugzilla 23731
#
# Revision 1.11  2003/02/17 16:48:43  ghillie
# - do locking, so that jobs can be deleted
#
# Revision 1.10  2003/02/10 14:50:52  ghillie
# - revert logic of outgoing_MSN: it's overriding the first number of
#  fax_numbers now
#
# Revision 1.9  2003/02/05 15:59:11  ghillie
# - search for *.txt instead of *.sff so no *.sff which is currently created
#  by capisuitefax will be found!
#
# Revision 1.8  2003/01/31 11:22:00  ghillie
# - use different sendq's for each user (in his user_dir).
# - use prefix user- for names in done and failed
#
# Revision 1.7  2003/01/27 21:56:46  ghillie
# - mailaddress may be not set, that's the same as ""
# - use first entry of fax_numbers as outgoing MSN if it exists
#
# Revision 1.6  2003/01/27 19:24:29  ghillie
# - updated to use new configuration files for fax & answering machine
#
# Revision 1.5  2003/01/19 12:03:27  ghillie
# - use capisuite log functions instead of stdout/stderr
#
# Revision 1.4  2003/01/17 15:09:26  ghillie
# - updated to use new configuration file capisuite-script.conf
#
# Revision 1.3  2003/01/13 16:12:00  ghillie
# - renamed from idle.pyin to idle.py as all previously processed variables
#  stay in the config file and cs_helpers.pyin now
#
# Revision 1.2  2002/12/16 13:07:22  ghillie
# - finished queue processing
#
# Revision 1.1  2002/12/14 13:53:19  ghillie
# - idle.py and incoming.py are now auto-created from *.pyin
#
</pre>

Aktuelle Version vom 20. August 2009, 14:24 Uhr

Zurück zum Portal

Capisuite Erweiterung

Bei der Umstellung von VBox unter Debian Sarge auf Capisuite unter Etch habe ich schmerzlich die Möglichkeit der individuellen Programmierung des Anrufbeantworters vermisst. Nachdem ich keine verbesserten incoming.py Files im Web gefunden habe, habe ich auf die schnelle Python gelernt und die fehlende Funktion selbst implementiert.

Individuelle Programmierung

In den Konfigfiles können Programme hinterlegt werden, die in Abhängigkeit von Zeit, Datum oder Telefonnummern (des Anrufers oder der Zielnummer) unterschiedlich reagieren. Ich nutze z.B. je eine eigene MSN für Privates, Geschäftliches und Fax. Die Geschäftsnummer klingelt nur Mo-Fr zw. 8:30 und 17:00, sonst aktiviert sich direkt der AB mit einer geschäftlichen Ansage. Wochenenden, Feiertage und Geburtstage sind zusätzlich hinterlegt. Familie und Freunde können uns Abends z.B. länger erreichen, als unbekannte Anrufer, die Nachrichten werden auch länger aufgezeichnet. Die Möglichkeiten sind nahezu beliebig.

  • Die Programme werden fortlaufend (!) nummeriert, beginnend bei prog1
  • Das erste Programm prog1 ... progX das match'ed (Zeit, Datum, etc.) wird verwendet, deshalb ist die Reihenfolge relevant!
  • Die Einträge dates und group können als eigene Section im Konfigfile definiert werden (siehe holiday, family, friends)
  • Die Telefonnummern müssen so definiert sein, wie sie im Logfile (/var/log/capisuite.log) erscheinen, d.h. üblicherweise 0911 12345678 (nicht: +49 (911) 12345678).
  • Es kann ein eigenes Telefonbuch verwendet werden. Einträge darin werden mit Namen gelistet (Email/Fax Email enthält den Namen und die Nummer)


Mein Konfig-File /etc/capisuite/answering_machine.conf sieht in etwa wie folgt aus und sollte weitestgehend selbsterklärend sein.

[GLOBAL]
audio_dir="/usr/share/capisuite/"
voice_user_dir="/var/spool/capisuite/users/"
user_audio_files="1"
voice_delay="15"
announcement="ab_buis.la"
record_length="60"
record_silence_timeout="5"
voice_email_from="Anrufbeantworter <ab@neobiker.de>"

[priv]
voice_numbers="12345678"
announcement="ab_priv.la"
voice_action="MailAndSave"
voice_delay="10"
record_length="90"
voice_email_from="ab@neobiker.de"
voice_email="Anrufbeantworter (Priv) <ab@neobiker.de>"
pin="1111"
# ----- vbox programming:
# ----- dates   time frame      week days       file to play    delay   record  group
prog1=  *       22:00-07:00     *               ab_priv.la      5       120     family,friends
prog2=  *       *               *               ab_priv.la      15      120     family,friends
prog3=  *       22:00-07:00     *               ab_priv.la      5       120     *
prog4=  *       *               *               ab_priv.la      10      120     *

[buis]
voice_numbers="22233345"
announcement="ab_buis.la"
voice_action="MailAndSave"
voice_delay="10"
record_length="60"
voice_email_from="Anrufbeantworter (Buis) <ab@neobiker.de>"
voice_email="ab@neobiker.de"
pin="2222"
# ----- vbox programming:
# ----- dates   time frame      week days       file to play    delay   record  group
prog1=  *       22:00-07:00     *               ab_priv.la      5       120     family,friends
prog2=  *       *               *               ab_priv.la      15      120     family,friends
prog3=  holiday *               *               ab_buis.la      1       90      *
prog4=  *       *               SA,SO           ab_buis.la      1       90      *
prog5=  *       17-08:29        *               ab_buis.la      1       90      *
prog6=  *       *               *               ab_buis.la      10      90      *

[holiday]
Weihnachten=24.12.,25.12.,26.12.
Ostern=6.4.2007,9.4.2007,17.5.2007,28.5.2007.,7.6.2007
Sonstige=1.1.,6.1.,1.5.,3.10.,1.11.
Birthdays=19.5.

Zusätzlich habe ich ein Telefonbuch definiert, in welchem auch die verschiedenen Gruppen (s.o. das Konfigfile / AB-Programme) definiert sind (Familie, Freunde, ...): /etc/capisuit/phonebook.conf

[GLOBAL]
Privat = 123456
Geschäft = 123457
Fax = 123458

[family]
Eltern=01234 12345, 0175123456,0172 234567
Oma=123456789

[friends]
Karl=12345,017134566789
Peter=0170123456, 0911 123412

[others]
Arzt = 12345
Werkstatt = 12346789
Buero = 123 345 567

Telefonnummern, welche nicht im Telefonbuch stehen, werden per online reverse lookup unter www.dasoertliche.de nachgeschlagen und sofern gefunden unter /var/cache/tbident/phonebook.cache gespeichert.

Der Anrufbeantworter (Fax dito) sendet mir eine Email mit einer Wav-Datei (oder PDF bei Fax) und den wichtigsten Info's:

From:    Anrufbeantworter (Priv) <ab@neobiker.de>
To:      ab@neobiker.de
Subject: Nachricht von Unbekannt fuer Privat
Anlage:  voice-184.wav

Anrufer: Unbekannt (0911987654321)
         
Laenge:  31 Sek.
Datum:   Di 11 Sep 2007 13:57:43 CEST
Nummer:  Privat (123456)

Siehe Anhang.
Das File wurde gespeichert unter: /var/spool/capisuite/users/ab/received/voice-184.la

Downloads

Download ISDN Files

Erweiterte Scripts

Ich habe die Files

  • incoming_adv.py
  • idle_adv.py
  • tbident.sh

erstellt. Die ersten beiden werden in /etc/capisuite/capisuite.conf aktiviert, das letzte zur Telefonbuch-Identifikation (tbident.sh) muss im Standardpfad installiert werden (z.B. unter /usr/bin/tbident.sh).

Die Scripts sind für eine Deutsche Ausgabe optimiert worden. Es wird einmal setlocale(de_DE) verwendet. Deshalb sind auf dem System die deutschen locales notwendig, diese können unter Debian mit dpkg-reconfigure locales erzeugt werden.

Hinweis: Die Scripts müssen zuerst compiliert werden!

Folgendes Script compiliert die CapiSuite Python Scripts:

#!/bin/sh
PYTHON=python2.4
DIRLIST=" /usr/lib/capisuite "
for i in $DIRLIST ; do
  $PYTHON -O /usr/lib/$PYTHON/compileall.py -q $i;
  $PYTHON /usr/lib/$PYTHON/compileall.py -q $i;
done

File: /usr/lib/capisuite/incoming_adv.py

Hier ist das erweiterte File /usr/lib/capisuite/incoming_adv.py.

File: /usr/lib/capisuite/idle_adv.py

Das idle.py File benötigt eine kleine Änderung (ganz unten im Script, mit '+' markiert), damit die neuen Sections nicht zu einer Fehlermeldung (über nicht existierenden System-User) führen.

#               idle_adv.py - default script for capisuite
#              ---------------------------------------------
#    copyright            : (C) 2007 neobiker
#    Version              : 1.1
#    Date                 : 03.01.2007
#
#    original script:
#    copyright            : (C) 2002 by Gernot Hillier
#    email                : gernot@hillier.de
#    version              : Revision: 1.8.2.2
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#

import os,re,time,pwd,fcntl
# capisuite stuff
import capisuite,cs_helpers

def idle(capi):
        config=cs_helpers.readConfig()
        spool=cs_helpers.getOption(config,"","spool_dir")
        if (spool==None):
                capisuite.error("global option spool_dir not found.")
                return

        done=os.path.join(spool,"done")+"/"
        failed=os.path.join(spool,"failed")+"/"

        if (not os.access(done,os.W_OK) or not os.access(failed,os.W_OK)):
                capisuite.error("Can't read/write to the necessary spool dirs")
                return

        userlist=config.sections()
        userlist.remove('GLOBAL')

        for user in userlist: # search in all user-specified sendq's
+               # neobiker: skip none user sections
+               if not (config.has_option(user,'voice_numbers') or config.has_option(user,'fax_numbers')):
+                       continue
                userdata=pwd.getpwnam(user)
                ...

File: /usr/bin/tbident.sh

Das Skript tbident.sh sucht mit lynx (muss installiert sein!) eine Telefonnummer online unter www.dasoertliche.de und cached jede Suche unter /var/cache/tbident/phonebook:

File /usr/bin/tbident.sh:

#!/bin/sh
# tbident.sh
#
# Rückwärtssuche Telefonnummer -> Eintrag im Telefonbuch
#
# Usage: tbident.sh "09123 34 45 67"
# Ursprung: altes tbident.sh des VDR-Portal's
#
# Gefundene Telefonnummern werden gecached unter:
# >>> /var/cache/tbident/phonebook <<<
#
# $Log: tbident.sh,v $
# Revision 1.4  2009/07/05 11:33:27  root
# added autom. updates of (grep) search parameters
#
# Revision 1.3  2008/06/05 11:35:00  root
# *** empty log message ***
#
# Revision 1.2  2008/06/05 11:33:27  root
# updated html syntax
#
# Revision 1.1  2007/09/09 15:50:43  root
# Initial revision
#

set -e

NR="$@"
NUMMER=`echo "$NR" | tr -d ' ()' | sed -e "s/^\+49//"`
NAME=""
DETAILS=""

URL="http://www.dasoertliche.de/?form_name=search_inv&action=1&&ph="
CONTEXT='Trefferliste'

CACHE=/var/cache/tbident/phonebook
TMPFILE=/tmp/tbident_$$
TMPVARS=/var/cache/tbident/tbident.vars
TIMEOUT=3

if [ ! -x /usr/bin/lynx ]; then
  echo "Error: Lynx not installed."
  exit 1
fi

if [ ! -e /usr/bin/tbident-check.sh ]; then
  echo "Error: tbident-check.sh not installed."
  exit 1
fi

if [ ! -e $CACHE ]; then
    mkdir -p `dirname $CACHE`
    touch $CACHE
fi
grep "^${NUMMER}|" $CACHE && exit 0

# read variables (daily per cron updated)
if [ ! -e "$TMPVARS" ]; then
    tbident-check.sh
fi
. $TMPVARS

# get 1.st match (-m1) of list
GREP="grep $CONTEXT $GREP_OPT -m1 $TMPFILE"

lynx "${URL}${NUMMER}" -dump -nolist -connect_timeout=$TIMEOUT >$TMPFILE 2>/dev/null

NAME=`$GREP | awk "{ if (NR == $NAME_LINE) print }" | sed -e "s/$NAME_FILTER\$//g" |
              sed -e 's/^ *//' -e 's/ *$//'`
DETAILS=`$GREP | awk "{ if (NR == $ADRS_LINE) print }" | sed -e "s/$ADRS_FILTER\$//g" |
              sed -e 's/^ *//' -e 's/ *$//'`

if [ -n "$NAME" ]; then
    echo "$NUMMER|$NAME|$DETAILS"
    echo "$NUMMER|$NAME|$DETAILS" >> $CACHE
else
    echo "$NR|Unbekannt|"
    echo "$NR|Unbekannt|" >> $CACHE
fi

rm $TMPFILE

File /usr/bin/tbident-check.sh:

#!/bin/sh
# tbident-check.sh
#
# Aktuelle Syntax prüfen und Variablen autom. setzen
# Rückwärtssuche Telefonnummer -> Eintrag im Telefonbuch
#

NR="091329040"
NUMMER=`echo "$NR" | tr -d ' ()' | sed -e "s/^\+49//"`

URL="http://www.dasoertliche.de/?form_name=search_inv&action=1&&ph="
CONTEXT='Trefferliste'
NAME=""
DETAILS=""

TMPFILE=/tmp/tbident_$$
TMPVARS=/var/cache/tbident/tbident.vars
TIMEOUT=3

if [ ! -x /usr/bin/lynx ]; then
  echo "Error: Lynx not installed."
  exit 1
fi

if [ ! -e "$TMPVARS" ]; then
    cat > $TMPVARS << EOT
NAME_LINE=0
ADRS_LINE=0
NAME_FILTER=
ADRS_FILTER=
GREP_OPT=
EOT
    chmod a+rx $TMPVARS
fi
mv $TMPVARS ${TMPVARS}.bak

lynx "${URL}${NUMMER}" -dump -nolist -connect_timeout=$TIMEOUT -dont_wrap_pre >$TMPFILE 2>/dev/null ||
     { echo "Error $0: Lynx failed."
       exit 1
     }

awk '
    BEGIN {
      srch="Trefferliste"
      nam="Herzo Werke GmbH"
      adr="Schießhausstr. 9, 91074 Herzogenaurach"
      srch_line=0
      nam_line=0
      adr_line=0
     }
    /Trefferliste/ { srch_line = NR }

    /Herzo Werke GmbH/ {
      nam_line = NR
      drop_nam = index($0, nam) + length(nam)
      drop_nam_str = substr($0, drop_nam)
      }
    /Schießhausstr. 9, 91074 Herzogenaurach/ {
      adr_line = NR
      drop_adr = index($0, adr) + length(adr)
      drop_adr_str = substr($0, drop_adr)
      }
    END {
      print "GREP_OPT=\"-A" max(adr_line, nam_line) - srch_line "\""
      print "NAME_LINE=\"" nam_line - srch_line + 1 "\""
      print "ADRS_LINE=\"" adr_line - srch_line + 1 "\""
      if ( length(drop_nam_str) ) printf "NAME_FILTER=\"%s\"\n", drop_nam_str
      else print "NAME_FILTER="
      if ( length(drop_adr_str) ) printf "ADRS_FILTER=\"%s\"\n", drop_adr_str
      else print "ADRS_FILTER="
    }
    function max(a,b)
    {
      if (a<b) return b
      else return a
    }
' $TMPFILE > $TMPVARS ||
       { echo "Error $0: Awk failed."
         mv ${TMPVARS}.bak $TMPVARS
         exit 1
       }

chmod a+rx $TMPVARS

diff $TMPVARS ${TMPVARS}.bak >/dev/null || echo "Info $0: Vars updated"
rm $TMPFILE

Zurück zum Portal