E-Mail Sprachmailbox mit RTPR-Server und Cisco IP Phone

Ich erhalte von meinem Servicerufnummeranbieter E-Mails mit Sprachnachrichten, die bei Abwesenheit für mich hinterlassen wurden. In den E-Mails befinden sich .wav Dateien mit den hinterlassenen Nachrichten. Wäre dies eine normale Sprachmailbox, könnte ich die Rufnummer einfach in meinem Cisco Telefon hinterlegen und die Sprachnachrichten per Telefonanruf abhören. Die .wav Dateien mit den Sprachaufzeichnungen aus den E-Mails lassen sich nicht so einfach über das Telefon abhören.

Auslesen des E-Mail Postfachs

Mit dieser PHP Funktion lese ich alle E-Mails in meinem Postfach, die einen bestimmten Betreff „$subject“ besitzen.

function imapMailList($user, $pass, $server, $subject){

	$mailbox = imap_open($server,$user,$pass,NULL, 1, 
			array('DISABLE_AUTHENTICATOR' => 'PLAIN'))or 
		           die(var_dump(imap_errors())); 

	$arr = imap_search($mailbox, $subject, FT_UID); 

	$data = imap_fetch_overview($mailbox, implode(",",$arr), FT_UID);

	for($i=0; $i <count($data); $i++){
		
		$data[$i]->number = getNumber($data[$i]->subject);
	}

	imap_close($mailbox);

	return $data;
}

Anmerkungen:

Der Username „$user“ und das Passwort „$pass“ sind die Postfachzugangsdaten, die auch für eine Einrichtung in einem E-Mail Programm verwendet werden. Der Servername wird wie folge erstellt.

$server = {ihre.server.adresse:993/imap/ssl}INBOX

Ich ermittle hier noch zusätzlich die Telefonnummer „$data[]->number“ über eine Parsing Funkion. Da die E-Mails nicht unbedingt bei jedem identisch aussehen, sollte hier jeder seine eigene Parsing Funktion erstellen. Die Ermittlung der Telefonnummer ist nicht zwingend notwendig, ermöglicht mir aber einen „Redail“ Button anzuzeigen, um den Anrufer direkt nach Abhören der Nachricht zurückzurufen.

Anzeige der E-Mails in einer Liste für die Sprachmailbox

Ich verwende für die Anzeige der Informationen die Template Engine Smarty. Dabei setze ich noch vor der Anzeige meiner Daten über das Template im PHP Script den Header „header(„Content-type: text/xml“);“

<?xml version="1.0" encoding="utf-8"?>
<CiscoIPPhoneIconFileMenu>
   <Title>E-Mail Messages</Title>
   <Prompt></Prompt>
{foreach $data as $item}
   <MenuItem>
	<IconIndex>{if $item->seen}0{else}1{/if}</IconIndex>
     <Name>From {$item->number}</Name>
     <URL>http://ihr.raspberry.server/messages.php?a=messages_detail&amp;b={$item->uid}</URL>
   </MenuItem>
{/foreach}
   <IconItem>
     <Index>0</Index>
     <URL>http://ihr.raspberry.server/PNG/letter_open.png</URL>
   </IconItem>
  <IconItem>
     <Index>1</Index>
     <URL>http://ihr.raspberry.server/PNG/letter.png</URL>
   </IconItem>
</CiscoIPPhoneIconFileMenu>

Anmerkungen:

Für gelesene und ungelesene Nachrichten blende ich hier unterschiedliche Symbole ein. Als Überschrift verwende ich die vorher ermittelte Telefonnummer.

CB-IT_Cisco_Phone_messages

Anzeigen einer E-Mail und .wav Datei aus Anhang speichern

Über den „Select“ Button öffnet sich ein neues Fenster auf dem Cisco Phone, das die E-Mail Details anzeigt. Im Hintergrund wird auf dem Raspberry Pi der Anhang der E-Mail gelesen und temporär auf als Datei auf dem Raspberry Pi abgelegt.

function imapMailContent($user, $pass, $server, $messageID){

	$mailbox = imap_open($server,$user,$pass,NULL, 1, 
			array('DISABLE_AUTHENTICATOR' => 'PLAIN'))or 
		           die(var_dump(imap_errors())); 

	$data = imap_fetch_overview($mailbox, $messageID, FT_UID); 

	imapMailAttachment($mailbox, $messageID);

	imap_close($mailbox);

	$data[0]->number = getNumber($data[0]->subject);

	return $data;
}

Anmerkungen:

Die „$massageID“ erhalte ich aus den Übergabeparametern, die ich beim Klicken auf den „Select“ Button über die aufgerufene URL mitgebe.

Den Anhang lese ich über die Funktion „imapMailAttachment“. Wenn ich einen Anhang ermitteln kann, werden im temporären Ordner „temp/“ Alle Dateien mit die mit „tmes“ beginnen gelöscht. Ich benötige für mein Telefon immer nur die aktuelle .wav Datei, daher leere ich das Verzeichnis komplett.

Danach wird der Anhang ausgelesen und in das Verzeichnis „/tmes“ auf dem Raspberry Pi Webserver kopiert. Um die .wav Datei wieder zu erkennen, benenne ich die Datei nach der E-Mail ID.

function imapMailAttachment($mailbox, $messageID) {

    $actMail = imap_fetchstructure($mailbox, $messageID, FT_UID);
    $PartsInActMail = count($actMail->parts);

    if ($PartsInActMail >= 2) 
    {
	for ($p=1; $p<$PartsInActMail; $p++)
	{

		$files = glob('temp/tmes*'); 
		foreach($files as $file){ 
			if(is_file($file))
			unlink($file); 
		}

		$inhalt = base64_decode(imap_fetchbody($mailbox, $messageID, $p+1, FT_UID));
		$fp = fopen("temp/tmes".$messageID.".wav", "w+"); 
		$files [$messageID] = "tmes".$messageID.".wav";
		fwrite ($fp, $inhalt) or die ("Error!"); 
		fclose ($fp);
	}
    }

}

Den Inhalt der E-Mail zeige ich erneut über ein Template an. Zuvor wird wieder der Header „header(„Content-type: text/xml“);“ gesetzt. Für das Abspielen der .wav Datei auf dem Cisco Telefon erstelle ich hier die Buttons „Play“ und „Stop“. Diese senden über „Notify“ nur den Status, dass abgespielt oder gestoppt werden soll an den Raspberry Pi Server.

<?xml version="1.0" encoding="utf-8"?>
<CiscoIPPhoneText>
   <Title>From {$data[0]->number}</Title>
   <Prompt></Prompt>
   <Text>{$data[0]->subject}&#13;&#13;{$data[0]->date|replace:" +0100":""} </Text>
 <SoftKeyItem>
   <Name>Update</Name>
   <URL>SoftKey:Update</URL>
   <Position>1</Position>
 </SoftKeyItem>
<SoftKeyItem>
    <Name>Play</Name>
   <URL>Notify:http:ihr.raspberry.server:80:messages.php?p={$data[0]->uid}</URL>
   <Position>2</Position>
 </SoftKeyItem>
<SoftKeyItem>
    <Name>Exit</Name>
   <URL>SoftKey:Exit</URL>
   <Position>3</Position>
 </SoftKeyItem>
<SoftKeyItem>
    <Name>Redail</Name>
   <URL>Dial:{$data[0]->number}</URL>
   <Position>4</Position>
 </SoftKeyItem>
<SoftKeyItem>
    <Name>Stop</Name>
   <URL>Notify:ihr.raspberry.server:80:messages.php?s={$data[0]->uid}</URL>
   <Position>5</Position>
 </SoftKeyItem>
</CiscoIPPhoneText>

Anmerkungen:

Es wird absichtlich nur ein „Notify“ an den Server gesendet, da hier keine weitere Oberfläche generiert werden muss. Es soll lediglich der Stream zum Telefon gestartet oder gestoppt werden. Der „Notify“ löst im nächsten Schritt den Stream vom Raspberry Pi Server zum Cisco Telefon aus.

Die Cisco Funktion „Play“, die für das Abspielen von Audiodateien standardmäßig verwendet werden kann, ist in der Abspiellänge begrenzt, sodass ich mich für längere Sprachnachrichten für den Stream entschieden habe.

CB-IT_Cisco_Phone_messages_detail

„Play“ oder „Stop“ des Audio Streams RTPR für die Sprachmailbox

Beim Klicken des „Play“ oder „Stop“ Buttons werden auf dem Raspberry Pi zwei Schritte ausgeführt. Der Stream muss gestartet werden und das Cisco Telefon muss per Push informiert werden, dass es den Stream empfangen soll. Zur Vereinheitlichung habe ich die XML Push Nachrichten über eine zentrale PHP Funktion gesteuert und benötige nur die Telefon IP Adresse „$phoneIP“, den Benutzernamen „$phoneUser“ und das Passwort „$phonePass“, sowie die Daten „$data“, die gesendet werden sollen.

function pushXMLToCisco( $phoneIP, $phoneUser, $phonePass, $data) {

	header("Content-type: text/xml");

	$auth = base64_encode($phoneUser.":".$phonePass);

	$xml = '<CiscoIPPhoneExecute><ExecuteItem URL="'.$data.'"/></CiscoIPPhoneExecute>';	
	$xml = 'XML='.urlencode($xml);

	$post = "POST /CGI/Execute HTTP/1.0\r\n";
	$post .= "Host: ".$phoneIP."\r\n";
	$post .= "Authorization: Basic $auth\r\n";
	$post .= "Connection: close\r\n";
	$post .= "Content-Type: application/x-www-form-urlencoded\r\n";
	$post .= "Content-Length: ".strlen($xml)."\r\n\r\n";

	$fp = fsockopen ( $phoneIP, 80, $errno, $errstr, 30);

	if(!$fp){ 
		echo "$errstr ($errno)<br>\nIP: $phoneIP<br>\n"; 
	} else {
		fputs($fp, $post.$xml);
		flush();
		$response= "";
		while (!feof($fp)) {

			$response .= fgets($fp, 128);
			flush();
		}
	}
}

Die Steuerung für RTPR „Play“

Zuerst schließe ich die „Notify“ Verbindung vom Telefon, um die weiteren Aufgaben abzuhandeln. Dann sende ich per XML Push den Befehl „RTPRx:“.$serverIP.“:20482:10″ zum Öffnen des Ports für den Stream auf das Telefon. Danach starte ich, wenn die .wav Datei vorhanden ist, den ffmpeg Stream. Falls weitere ffmpeg Streams vorhanden sind, werden diese geschlossen. Ist der Stream abgeschlossen, wird auch auf dem Telefon per XML Push die Übertragung beendet.

if(isset($_GET['p']) {

	ignore_user_abort(true);
	set_time_limit(0);

	ob_start();

	header('Connection: close', true);
	header('Content-Length: 0',true);
	
	flush();
	ob_flush();

	$data = "RTPRx:".$serverIP.":20482:10";
	pushXMLToCisco($phoneIP, $phoneUser, $phonePass, $data);


	if(is_file("temp/tmes".$_GET['p'].".wav")){

		shell_exec('pkill -f ffmpeg');

		shell_exec("ffmpeg -re -i /xxx/xxx/xxx/temp/tmes".$_GET['p'].".wav -filter_complex 'aresample=8000,asetnsamples=n=160' -f rtp rtp://".$phoneIP.":20482");

	}

	$data = "RTPRx:Stop";
	pushXMLToCisco($phoneIP, $phoneUser, $phonePass, $data);
}

Die Steuerung für RTPR „Stop“

Über den „Stop“ Button ist es möglich den Stream der .wav Datei jederzeit abzubrechen. Hierzu wird auf dem Raspberry Pi Server der ffmpeg Stream abgebrochen und eine XML Push Nachricht an das Cisco Telefon gesendet, um die Übertragung auch dort zu beenden.

if(isset($_GET['s'])) {

	ignore_user_abort(true);
	set_time_limit(0);

	ob_start();

	header('Connection: close', true);
	header('Content-Length: 0',true);
	
	flush();
	ob_flush();

	shell_exec('pkill -f ffmpeg');

	$data = "RTPRx:Stop";
	pushXMLToCisco($phoneIP, $phoneUser, $phonePass, $data);

}

Chrome to Cisco Phone

In diesem Beitrag Chrome to Cisco Phone möchte ich zeigen, wie ich mir eine Erweiterung (Add-on) für den Chrome Browser geschrieben habe, um eine markierte Telefonnummer an mein Cisco IP Telefon zu senden.
Ich nehme es vorweg, dass es solche Erweiterungen schon gibt. Das ist aber nicht der Anspruch dieses Artikels über etwas vollkommen Neues zu berichten. Vielmehr möchte ich den Programmcode meiner Erweiterung öffentlich zugänglich machen und auch für mich dokumentieren, welche Schritte für ein solches Add-on notwendig sind. Es ist natürlich auch möglich, die Erweiterung noch weiter auszubauen. Icons hinzuzufügen, die Optionen einstellbar zu machen oder spezielle Validierungen durchzuführen, die die Telefonnummer weiter prüfen.

Ich möchte mit dieser Erweiterung erreichen, dass ich eine Telefonnummer auf einer beliebigen Internetseite markieren kann und über einen Button im Kontextmenü diese Nummer dann direkt an mein Cisco IP Telefon übertragen kann. Dort wird die Nummer direkt gewählt. Dazu möchte ich eine Chrome-Erweiterung erstellen, die eine XML-Datei generiert. Diese XML-Datei sende ich dann direkt an das Telefon.

CB-IT_Services_Send_to_Cisco_Phone

Beginnen wir mit der Erweiterung (Add-on), die wir für den Browser benötigen. Ich habe in einem Ordner zwei Dokumente angelegt:

  • manifest.json
  • sentNumberToCisco.js

Chrome manifest.json

In der manifest.json Datei werden die Add-on benötigten Einstellungen, Deklarationen, Berechtigungen sowie Name und Beschreibung eingetragen. Ich habe mich hierbei an die von Chrome veröffentlichte Vorgabe Manifest File Format gehalten.

Hier der Auszug aus meiner „manifest.json“ Datei :

{
	"name": "Send Number to Cisco Phone",
	"version": "0.1",
	"description": "Mark a phone number on a website and press the button in context menü.",
	"author": "ChrisBue Blog",
	"permissions": [ 
		"contextMenus",
		"http://*/*" ],
	"background": {
		"persistent": false,
		"scripts": ["sendNumberToCisco.js"]
	},
	"manifest_version": 2	
}

Anmerkungen:

„permissions“: Ich benötige die „contextMenus“ Berechtigung, um einen Button dem Kontextmenü hinzuzufügen. Die „http://*/*“ Berechtigung ermöglicht mir das Senden der XML-Datei an das Telefon. Ohne diese Berechtigung kann keine Übertragung nach außen erfolgen. Die „storage“ Berechtigung ist optional. Ich verwende den Chrome-Speicher „storage“ um meine Optionen abzulegen.

„background“: Hier lege ich fest, welches Script ich ausführen möchte. In diesem Fall verwende ich das „sendNumberToCisco.js“ Skript. Darin werde ich die notwendigen Funktionen unterbringen, um die Nummer direkt zu senden und den Button im Kontextmenü zu verankern.

Chrome sendNumberToCisco.js

In der Javascript-Datei „sendNumberToCisco.js“ für das Chrome to Cisco Phone Projekt werden alle Funktionen erstellt, die den zusätzlichen Eintrag im Kontextmenü erzeugen, die XML-Datei mit der Telefonnummer erstellen und die Verbindung zum Telefon herstellen. Als Erstes erstellen wir den Button im Kontextmenü.

chrome.runtime.onInstalled.addListener(function() {
	// Create an item for a selected text in the menu
	var id = chrome.contextMenus.create({"title": "Send to Cisco phone", "contexts":["selection"], "id": "numberToCiscoPhoneSelection"});
});

Anmerkungen:

Ich erstelle einen Listener, der dem Kontextmenü einen Eintrag mit dem Namen „Send to Cisco phone“ hinzufügt. Die Option „contexts“: [„section“] ermöglicht es mir, dass der Eintrag im Kontextmenü nur angezeigt wird, wenn auch tatsächlich etwas markiert ist. Natürlich bekommt der Button auch eine „id“: „numberToCiscoPhoneSelection“.

Jetzt erstellen ich einen weiteren Listener, der auf den Klick im Kontextmenü reagiert und die entsprechenden Funktionen ausführt, die es ermöglichen die Daten an das Telefon zu senden. Vorher validiere ich noch notdürftig die Telefonnummer. Dies lässt sich bestimmt noch weiter ausbauen, genügt aber für meine Zwecke.

// The onClicked callback function.
function onClickHandler(info, tab) {

	// Check if the correct button is clicked
	if(info.menuItemId == "numberToCiscoPhoneSelection"){

		var ip = "000.000.000.000";
		var uid = "XXXX";
		var pwd = "XXXX";
	
		if(info.selectionText == undefined){
			alert("nothing marked!");
			exit;
		} 

		// Replace '+' to '00'
		var number = info.selectionText.replace('+', '00')
		// Only numbers
		number = number.replace(/\D+/g,"");

		// Check if there is a number to call
		if(number == ""){
			alert("nothing marked!");
			exit;
		}

		// Build XML
		var xml = '<?xml version="1.0"? encoding="UTF-8">\r\n' +
		'<CiscoIPPhoneExecute>' +
		'<ExecuteItem Priority="0" URL="Dial:' + number + ':1:Cisco/Dialer"/>\r\n'+
		'</CiscoIPPhoneExecute>\r\n';

		// XML request
		var xhttp = new XMLHttpRequest();
		xhttp.open("POST", "http://"+ip+"/CGI/Execute", true);
		xhttp.setRequestHeader ("Authorization", "Basic " + btoa(uid + ":" + pwd));
		xhttp.setRequestHeader ("Content-type", "application/x-www-form-urlencoded");
		xhttp.onreadystatechange = function(){

			// Log response from phone for connection debug
			console.log( this.responseText );
		}
		xhttp.send("XML="+xml);		
	}
};

chrome.contextMenus.onClicked.addListener(onClickHandler);

Anmerkungen:

Ich erstelle einen „chrome.contextMenus.onClicked.addListener“ der den „onClickHandler“ aufruft. Ich prüfe ab, ob das geklickte Objekt die selbe ID hat wie der Eintrag, den ich dem Kontextmenü hinzugefügt habe („numberToCiscoPhoneSelection“).

Die Variablen „ip“, „uid“ und „pwd“ sollten hier nicht hart codiert eingetragen werden. Es gibt eine schönere Möglichkeit diese Daten z. B. im Speicher von Google Chrome abzulegen.

In den nächsten Schritten prüfe ich, ob auch Daten an die Funktion übergeben wurde. Ich ändere das „+“ in „00“ um, da oft „+49“ in den Telefonnummern angegeben ist. Danach entferne ich alles was, keine Zahl ist aus der Telefonnummer, z. B. – / oder Leerzeichen.

Jetzt das Wichtigste. Ich erstelle die XML-Datei, die ich übertragen möchte. Ich habe den genauen Aufbau der XML-Datei der Cisco Dokumentation entnommen.
Ich erstelle nun ein Objekt um XML Daten zu senden „new XMLHttpRequest();“, füge die Zieladresse ein „http://“+ip+“/CGI/Execute“ und melde mich am Telefon an „Authorization“, „Basic “ + btoa(uid + „:“ + pwd)“.

Zum Schluss kann ich die Daten senden „xhttp.send(„XML=“+xml);“. Als Hilfe lasse ich mir die Antwort des Telefons in der Konsole von Chrome anzeigen.

Um nun die Erweiterung (Add-on) in Chrome zu laden, muss ich zuerst in den Einstellungen die Erweiterungen aufrufen. Hier den Haken für den „Entwicklermodus“ setzten und über den Dialog hinter „Entpackte Erweiterung laden …“ den Ordner mit den zwei Dateien auswählen.