Dialing from the Emacs BBDB address book with least-cost routing

Abstract: I don't type in phone numbers any more. A spare analog modem, connected in parallel to my phone, does the whole job. I type M-d into my address book, the Big Brother Database, and it will dial the stored number, using the cheapest provider.

  1. Get isdn4k-utils from isdn4linux.de; don't worry, you don't actually need to have ISDN, we just need the isdnrate utility for finding the best provider. The "stable" versions won't work, so get some CVS snapshot instead.

  2. Get recent provider data from rates4linux.sourceforge.net. Fix /etc/isdn/rates.conf to list the providers of your choice.

  3. Get BBDB, the Insidious Big Brother Database, by Jamie Zawinski, version 2.00.06. Put all addresses and phone numbers into the database, and get rid of your old address book.

  4. Connect a modem to the same telephone line as your phone. If you have a TAE socket as is usual in Germany, plug the modem into an "N" socket. This way, when the modem is off-hook, the phone plugged into the "F" socket gets disconnected, so you won't have to listen to the DTMF tones.

  5. Try dialing with it via minicom, then write a chat script like this, /usr/local/etc/dial-chat:

    SAY "Dialing \T...\n"
    ABORT 'BLACKLISTED'
    ABORT 'NO DIALTONE'
    ECHO ON
    '' ATM0X1DT\T; OK ath0
    

    The trick is to have the modem return to command mode after dialing, instead of waiting for a modem carrier. This is accomplished by typing a semicolon after the number in the ATD command. When the modem returns to command mode, we hang up immediately, so the off-hooked phone gets the line again.

  6. Here are a few trivial scripts that I use:

  7. This is bbdb-phone.el, which does some nice things with European phone numbers. It also provides the M-x dial command.
    ;;; bbdb-phone.el --- BBDB/Isdnlog/ESTIC integration
    
    ;; Copyright (C) 1999 by Free Software Foundation, Inc.
    
    ;; Author: Matthias Koeppe <mkoeppe@moose.boerde.de>
    ;; Keywords: local
    
    ;; This file is NOT part of GNU Emacs.
    
    ;; 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, or (at
    ;; your option) any later version.
    
    ;; This program is distributed in the hope that it will be useful, but
    ;; WITHOUT ANY WARRANTY; without even the implied warranty of
    ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    ;; General Public License for more details.
    
    ;; You should have received a copy of the GNU General Public License
    ;; along with this program; see the file COPYING.  If not, write to
    ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
    ;; Boston, MA 02111-1307, USA.
    
    ;;; Commentary:
    
    ;; BBDB & phone integration
    
    ;;; Code:
    
    (require 'bbdb)
    (require 'bbdb-com)
    
    (defvar phone-area-prefix "391")
    (defvar phone-area-internal-prefix "ISTEC")
    (defvar dialout-prefix "0,")
    
    (defun my-canonicalize-phone-number (number)
      "Canonicalize phone number NUMBER, which must be a string."
      (cond ((= (aref number 0) ?+) number)	; already has international prefix
    	((= (aref number 0) ?0) (concat "+49 " (substring number 1)))
    	(t (concat "+49 " phone-area-prefix number))))	 
    
    (defun my-localize-phone-number (number)
      "Make NUMBER ready for dialing from local site."
      (cond ((string-match (concat "^\\(\\++49[^0-9]*\\|0\\)" 
    			       phone-area-prefix
    			       "[^0-9]*")
    		       number)		; make local number
    	 (substring number (match-end 0)))
    	((string-match "^\\++49[^0-9]*"
    		       number)		; make long-distance number
    	 (concat "0" (substring number (match-end 0))))
    	((string-match "^\\++"
    		       number)		; make international number
    	 (concat "00" (substring number (match-end 0))))
    	(t number)))			; number is ok
    
    (defun my-strip-nondigits (string)
      "Return STRING stripped of all non-digits."
      (while (string-match "[^0-9]+" string)
        (setq string (replace-match "" t t string)))
      string)
    
    (defun my-strip-nondigitsplus (string)
      "Return STRING stripped of all non-digit non-plus characters."
      (while (string-match "[^0-9+]+" string)
        (setq string (replace-match "" t t string)))
      string)
    
    (defun my-extract-phone-aliases-from-bbdb ()
      "Extract phone aliases from BBDB."
      (let ((records (bbdb-records))
    	(isdnlog-alias-buffer (get-buffer-create " *isdnlog-alias*"))
    	(estic-alias-buffer (get-buffer-create " *estic-alias*")))
        (while (not (null records))
          (let* ((record (car records))
    	     (phones (bbdb-record-phones record))
    	     (name (bbdb-record-name record))
    	     (comp (bbdb-record-company record))
    	     (name-comp 
    	      (cond ((and name comp) (concat name " - " comp))
    		    ((or name comp))
    		    (t "???"))))	
    	(while phones
    	  (let* ((phone (car phones))
    		 (location (aref phone 0))
    		 (number (aref phone 1))
    		 (name-comp-loc
    		  (concat name-comp " (" location ")")))
    	    (set-buffer isdnlog-alias-buffer)
    	    (insert "[number]\n"
    		    "NUMBER = " (my-canonicalize-phone-number number) "\n"
    		    "SI = 1\n"		; service indicator is voice
    		    "ZONE = 1\n"	; this is incorrect but who cares?
    		    "ALIAS = " name-comp-loc "\n\n")
    	    (set-buffer estic-alias-buffer)
    	    (insert (my-strip-nondigits (my-localize-phone-number number))
    		    " \"" name-comp-loc "\"\n"))
    	  (setq phones (cdr phones))))
          (setq records (cdr records)))
        (set-buffer isdnlog-alias-buffer)
        (set-buffer-file-coding-system 'latin-1-unix)
        (write-file "~/.isdnlog-alias")
        (kill-buffer isdnlog-alias-buffer)
        (set-buffer estic-alias-buffer)
        (set-buffer-file-coding-system 'latin-1-unix)
        (write-file "~/.estic-alias")
        (kill-buffer estic-alias-buffer)))
    
    (defvar incoming-phone-messages-file "/var/log/messages")
    
    (defun last-unnamed-caller (output-to-buffer-p)
      "Show the phone number of the last unnamed caller.
    With prefix argument, insert in current buffer."
      (interactive "P")
      (let ((buffer (get-buffer-create " *last-caller*"))
    	(old-buffer (current-buffer)))
        (set-buffer buffer)
        (let ((len (nth 7 (file-attributes incoming-phone-messages-file))))
          (insert-file-contents incoming-phone-messages-file nil 
    			    (max 0 (- len 20000)) len))
        (goto-char (point-max))
        (if (search-backward-regexp "Call from \\(\\+[0-9]+ [0-9]+/[0-9]+\\).*RING" nil t)
    	(if output-to-buffer-p
    	    (progn
    	      (set-buffer old-buffer)
    	      (insert-buffer-substring buffer (match-beginning 1) (match-end 1)))
    	  (message "Last unnamed caller: %s" 
    		   (buffer-substring (match-beginning 1) (match-end 1))))
          (error "No unnamed caller."))
        (set-buffer old-buffer)
        (kill-buffer buffer)))
    
    (defvar outgoing-phone-messages-file "/var/log/estic-outgoing.log")
    
    (defun last-unnamed-destination (output-to-buffer-p)
      "Show the phone number of the last unnamed call destination.
    With prefix argument, insert in current buffer."
      (interactive "P")
      (let ((buffer (get-buffer-create " *last-caller*"))
    	(old-buffer (current-buffer)))
        (set-buffer buffer)
        (let ((len (nth 7 (file-attributes outgoing-phone-messages-file))))
          (insert-file-contents outgoing-phone-messages-file nil 
    			    (max 0 (- len 20000)) len))
        (goto-char (point-max))
        (if (search-backward-regexp "Called \\([0-9]+\\) with" nil t)
    	(if output-to-buffer-p
    	    (progn
    	      (set-buffer old-buffer)
    	      (insert-buffer-substring buffer (match-beginning 1) (match-end 1)))
    	  (message "Last unnamed call destination: %s" 
    		   (buffer-substring (match-beginning 1) (match-end 1))))
          (error "No unnamed call destination."))
        (set-buffer old-buffer)
        (kill-buffer buffer)))
    
    (defun provider-prefix (number)
      "Return a provider prefix for dialing canonical NUMBER."
      "")
    
    (defun my-localize-phone-number (number)
      "Make canonical NUMBER ready for dialing from local site."
      (let ((num (my-strip-nondigitsplus number))
    	(provider-prefix (provider-prefix number)))
        (cond ((string-match (concat "^\\(\\++49[^0-9]*\\|0\\)" 
    				 phone-area-prefix
    				 phone-area-internal-prefix
    				 "[^0-9]*")
    			 num)	; make internal number
    	   (substring num (match-end 0)))
    	  ((and (string= provider-prefix "")
    		(string-match (concat "^\\(\\++49[^0-9]*\\|0\\)" 
    				      phone-area-prefix
    				      "[^0-9]*")
    			      num))	; make local number
    	   (concat dialout-prefix  
    		   (substring num (match-end 0))))
    	  ((string-match "^\\++49[^0-9]*"
    			 num)	; make long-distance number
    	   (concat dialout-prefix 
    		   provider-prefix 
    		   "0" 
    		   (substring num (match-end 0))))
    	  ((string-match "^\\++"
    			 num)	; make international number
    	   (concat dialout-prefix 
    		   provider-prefix
    		   "00" 
    		   (substring num (match-end 0)))))))
    
    ; My own version
    (defun bbdb-dial (phone force-area-code)
      "On a Sun SparcStation, play the appropriate tones on the builtin 
    speaker to dial the phone number corresponding to the current line.
    If the point is at the beginning of a record, dial the first phone
    number."
      (interactive (list (bbdb-current-field)
    		     current-prefix-arg))
      (if (eq (car-safe phone) 'name)
          (setq phone (car (bbdb-record-phones (car (cdr phone))))))
      (if (eq (car-safe phone) 'phone)
          (setq phone (car (cdr phone))))
      (or (vectorp phone) (error "not on a phone field"))
      (or window-system (error "You're not under window system."))
      (or (file-exists-p bbdb-sound-player)
          (error "no sound player program"))
      (let* ((str (my-localize-phone-number 
    	       (my-canonicalize-phone-number 
    		(bbdb-phone-string phone)))))
        (bbdb-dial-string str)))
    
    (defun dial (phone)
      "Dial the given phone number."
      (interactive "sNumber: ")
      (let* ((str (my-localize-phone-number 
    	       (my-canonicalize-phone-number phone))))
        (bbdb-dial-string str)))
    
    (provide 'bbdb-phone)
      
    ;;; bbdb-phone.el ends here
    
  8. Up to here, the BBDB dialing code still wants to dial the phone number via the speaker, as it does on my Sun workstation in the office. But on the GNU/Linux box at home, I have the following in my .emacs:
    ;;; Dialling
    
    (require 'bbdb-phone)
    
    (defun bbdb-dial-string (s)
      (message "Dialing %s..." s)
      (call-process "dial-number" nil nil nil s)
      (message "Dialing %s...done." s))
    
    (defvar my-provider "01033") ;; DTAG
    
    (defun provider-prefix (number)
      "Return a provider prefix for dialing canonical NUMBER."
      (interactive "sShow best providers for number: ")
      (set-buffer (get-buffer-create "*Best providers*"))
      (erase-buffer)
      (call-process "fast-isdnrate" nil t nil
    		"-o"			; only those listed in rate.conf
    		(my-strip-nondigitsplus number))
      (pop-to-buffer (current-buffer))
      (shrink-window-if-larger-than-buffer)
      (sit-for 0)
      (goto-char (point-min))
      (if (re-search-forward (concat "^" my-provider "_[0-9]*:.*") nil t)
          (put-text-property (match-beginning 0) 
    			 (match-end 0)
    			 'face 'bold))
      (goto-char (point-min))
      (if (looking-at "\\([0-9]*\\)_[0-9]*:")
          (if (string= (match-string 1) my-provider)
    	  ""
    	(concat (match-string 1) ","))
        (error "Huh? Didn't find provider?")))
    
    (setq bbdb-sound-player "/bin/true")
    
  9. Here is a summary of the functionality:
Matthias Koeppe