#!/usr/bin/env python
"""Minimal Voice Mail message browser

$Id: mvmmessage,v 1.5 1999/05/22 16:12:19 dan Exp dan $

Copyright © 1998-1999 by Dan Fandrich <dan@fch.wimsey.bc.ca>

This was quickly hacked together to provide a simple user interface to the
voice messages recorded by vgetty.  It runs under Python with the Tkinter
extensions.

Usage:
  mvmmessage [mailbox#]

To do:
 - create a button to cancel a playing sound
   - change Play button to Stop and display Play popup entry
   - need to somehow wait for the child process to exit, then switch things
     back to normal when it does (start a timer?)
 - make middle button archive a message
 - instead of deleting messages, move them to a .deleted directory and delete
   them only when the program quits
 - after deleting a message, select the next one automatically
 - make default size of message box bigger
 - create nice pictures for the buttons
 - create a hypertext link in the About box to the URL
   - try to call netscape, arena, mosaic, then finally xterm -e lynx

$Log: mvmmessage,v $
Revision 1.5  1999/05/22 16:12:19  dan
Added check for incoming directory if mailbox doesn't exist. Improved
play command. Added icon when minimized. Added About button.
Added scroll bar to message list window.

Revision 1.4  1998/05/31  23:50:27  dan
Added pop-up menu on right click on message. Added method comments.

Revision 1.3  1998/05/07  06:23:03  dan
Updated comments.

Revision 1.2  1998/05/07  05:47:27  dan
Cleaned up configuration options; now creating ARCHIVE_DIR automatically
when necessary.

Revision 1.1  1998/05/07  05:12:30  dan
Initial revision

"""

# mvmmessage configuration options

# vgetty configuration file
CONFIG_FILE = '/etc/mgetty+sendfax/voice.conf'

# sample rate of sound files
SAMPLE_RATE = 7200

# bits per sample of recorded sounds (2, 4 or 8 depending on compression used)
BITS_PER_SAMPLE = 2


# ---------------------------------------------------
# The remaining options should not need to be changed

# directory containing voice mailboxes
MAILBOX_DIR = 'mailboxes'# relative to spool directory

DEF_MAILBOX = '0'		# default mailbox number

# alternate directory containing messages (used in bare vgetty installation)
ALT_MAILBOX_DIR = 'incoming'	# relative to spool directory

# directory containing archived voice mail
ARCHIVE_DIR = '.archive' # relative to mailbox directory

# permission of archive directory if it needs to be created
ARC_DIR_PERM = 0700

# Command to play a rmd compressed audio file to the speaker
# rmdtopvf and pvftoau are from the mgetty-voice package, play is from sox
# Note: the -x flag might not be needed on big-endian machines
PLAY_CMD = 'rmdtopvf %s | pvftolin -W | sox -t uw -x -r ' + `SAMPLE_RATE` + ' - -t au -r 8000 /dev/audio'

# default root directory of voice mail spool if not in CONFIG_FILE
VMAIL_SPOOL_DIR = '/var/spool/voice'

# bits per second of recorded speech
RATE = BITS_PER_SAMPLE * SAMPLE_RATE

# icon to use
ICON = 'phone_3d.xbm'

RCSrevision = "$Revision: 1.5 $"
__version__ = RCSrevision[11:-2]

# Message displayed in About box
aboutmsg = 'Minimal Voice Mail Message version '+__version__+ '''
Copyright 1998-1999 by Dan Fandrich
dan@fch.wimsey.bc.ca
<http://www.npsnet.com/danf/software/>

MVM Message comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under terms of the GNU General Public License; see the
included file COPYING for details.
'''

# Program icon 
icondata = \
'''#define phone_3d_width 50
#define phone_3d_height 40
static char phone_3d_bits[] = {
 0x00,0x00,0x00,0x00,0x00,0x00,0xfc,0x00,0x00,0x00,0x00,0x00,0x00,0xfc,0x00,
 0xf0,0xff,0xff,0x00,0x00,0xfc,0x00,0x10,0x00,0x80,0x01,0x00,0xfc,0x00,0x10,
 0x00,0x80,0x02,0x00,0xfc,0x00,0x10,0x00,0x80,0x04,0x00,0xfc,0x00,0x10,0x00,
 0x80,0x08,0x00,0xfc,0x00,0x10,0x00,0x80,0x10,0x00,0xfc,0x00,0x10,0x00,0x80,
 0x3f,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,
 0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,
 0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,
 0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,
 0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,
 0xf8,0x7f,0x20,0x00,0xfc,0x00,0x10,0xff,0xff,0x23,0x00,0xfc,0x00,0x10,0xff,
 0xff,0x23,0x00,0xfc,0x00,0x90,0x1f,0xc0,0x27,0x00,0xfc,0x00,0x90,0xc7,0x1f,
 0x27,0x00,0xfc,0x00,0x10,0xd8,0xdf,0x20,0x00,0xfc,0x00,0x10,0xde,0xdf,0x23,
 0x00,0xfc,0x00,0x10,0xbe,0xef,0x23,0x00,0xfc,0x00,0x10,0x7e,0xf0,0x23,0x00,
 0xfc,0x00,0x10,0xfe,0xff,0x23,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,
 0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,
 0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,
 0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,0x00,0x20,0x00,0xfc,0x00,0x10,0x00,
 0x00,0x20,0x00,0xfc,0x00,0xf0,0xff,0xff,0x3f,0x00,0xfc,0x00,0x00,0x00,0x00,
 0x00,0x00,0xfc,0x00,0x00,0x00,0x00,0x00,0x00,0xfc};
'''

# Start of program

from Tkinter import *
from string import atoi, split
from stat import *
from sys import argv, exit, stderr
import os
import regex
import tempfile

def printerr(msg):
	'''Print an error message to stderr including leading program name

	Displays a message of the form:
	 progname: This is an error or warning message
	'''
	stderr.write('%s: %s\n' % (os.path.basename(sys.argv[0]), msg))

def fileExists(fn):
	"Returns TRUE if the file exists, FALSE if it does not or could not tell"
	try:
		st = os.stat(fn)
		return 1	# file exists
	except os.error:
		return 0	# file does not exist or is not readable

def getSpoolDir():
	"Get the voice spool directory from the vgetty config file"
	try:
		lines = open(CONFIG_FILE).readlines()
	except IOError:
		return VMAIL_SPOOL_DIR

	for line in lines:
		param = split(line)
		if len(param) >= 2:
			if param[0] == 'voice_dir':
				return param[1]
	return VMAIL_SPOOL_DIR

class MvmMessages(Frame):
	"Voice message GUI class"
	def __init__(self, parent=None):
		Frame.__init__(self, parent)
		self.pack(fill=BOTH, expand=YES)
		self.createWidgets()

	def createWidgets(self):
		self.createToolbar()
		self.createPopup()
		self.createMessageList()

	def createMessageList(self):
		# Create a frame for the message list and scroll bar
		self.messageFrame = Frame(self)
		self.messageFrame.pack(fill=BOTH, expand=YES)

		self.messageList = Listbox(self.messageFrame)
		self.messageList.pack(side=LEFT, fill=BOTH, expand=YES)
		self.messageList.bind('<Double-Button-1>', self.playMessage)
		self.messageList.bind('<Button-3>', self.msgPopup)

		# Add in a scroll bar on the right hand side
		self.scrollBar = Scrollbar(self.messageFrame)
		self.messageList['yscrollcommand'] = self.scrollBar.set
		self.scrollBar['command'] = self.messageList.yview
		self.scrollBar.pack(side=RIGHT, fill=Y)

	def createToolbar(self):
		"Create buttons at the top of the window"
		self.toolbar = Frame(self)
		self.toolbar.pack(side=TOP)

		self.quitbutton = Button(self.toolbar, text='Quit')
		self.quitbutton['command'] = self.quit
		self.quitbutton.pack(side=LEFT)

		self.delbutton = Button(self.toolbar, text='Delete')
		self.delbutton['command'] = self.delMessage
		self.delbutton.pack(side=LEFT)

		self.arcbutton = Button(self.toolbar, text='Archive')
		self.arcbutton['command'] = self.arcMessage
		self.arcbutton.pack(side=LEFT)

		self.playbutton = Button(self.toolbar)
		self.playbutton.PlayMsg = 'Play'
		self.playbutton.StopMsg = 'Stop'
		self.playbutton['text'] = self.playbutton.PlayMsg
		self.playbutton['command'] = self.playMessage
		self.playbutton.pack(side=LEFT)

		self.refreshnbutton = Button(self.toolbar, text='Refresh')
		self.refreshnbutton['command'] = self.refreshDir
		self.refreshnbutton.pack(side=LEFT)

		self.scanbutton = Button(self.toolbar)
		self.scanbutton.viewArcMsg = ' View Archive '
		self.scanbutton.viewIncMsg = 'View Incoming'
		self.scanbutton['text'] = self.scanbutton.viewArcMsg
		self.scanbutton['command'] = self.switchArchive
		self.scanbutton.pack(side=LEFT)

		self.aboutbutton = Button(self.toolbar, text='About')
		self.aboutbutton['command'] = self.about
		self.aboutbutton.pack(side=LEFT)

	def about(self):
		"Display window with program information"

		ab = Toplevel(self)
		ab.mes = Frame(ab, bd=2, relief=SUNKEN)
		ab.mes.pack(side=TOP, padx=5, pady=7)
		
		ab.title('About MVM Message')
		Message(ab.mes, relief=RAISED,
						font='-*-helvetica-bold-r-normal--16-*-*-*-*-*-iso8859-1',
						text=aboutmsg,
						width='1000').pack(side=TOP)
						
		Button(ab, text='OK', 
						command=ab.destroy).pack(side=BOTTOM, padx=6, fill=X)

	def createPopup(self):
		"Pop-up menu on right click on a message"
		self.popup = Menu(self)
		self.popup['tearoff'] = FALSE
		self.popup.add_command(label='Play', command=self.playMessage)
		self.playPopupEntry = 0	# Item to disable when playing not allowed
		self.popup.add_command(label='Archive', command=self.arcMessage)
		self.arcPopupEntry = 1	# Item to disable when archiving not allowed
		self.popup.add_command(label='Delete', command=self.delMessage)

	def msgPopup(self, event):
		"Display pop-up menu on right click on a message"
		if self.messageList.size() < 1:
			return
		self.messageList.select_clear(0,self.messageList.size())
		self.messageList.select_set(self.messageList.nearest(event.y))
		self.popup.tk_popup(event.x_root, event.y_root)

	def scanMessages(self, dir, arcDir):
		"See what messages are available and display a list"
		self.incomingDir = dir
		self.arcDir = arcDir

		self.messageDir = self.incomingDir
		self.refreshDir()

	def refreshDir(self):
		"Create a list of messages in the message directory"
		self.messageList.delete(0, END)
		msg_re = regex.symcomp('^msg_\(<year>[0-9][0-9]\)'
								'\(<mon>[0-9][0-9]\)'
								'\(<day>[0-9][0-9]\)'
								'\(<hour>[0-9][0-9]\)'
								'\(<min>[0-9][0-9]\)'
								'\(<sec>[0-9][0-9]\)'
								'_from_\(<from>-*[0-9]*\)'
								)
		try:
			filelist = os.listdir(self.messageDir)
		except os.error:
			filelist = []

		# Filter out all non-message files
		self.messageFiles = filter(lambda fn, msg_re=msg_re: msg_re.search(fn) >= 0,
							filelist)

		self.messageFiles.sort()

		# Put all messages into the listbox
		count = 0
		for fn in self.messageFiles:
			count = count + 1
			msg_re.search(fn)
			year = atoi(msg_re.group('year'))
			mon = atoi(msg_re.group('mon'))
			day = atoi(msg_re.group('day'))
			hour = atoi(msg_re.group('hour'))
			min = atoi(msg_re.group('min'))
			sec = atoi(msg_re.group('sec'))
			fromMbox = atoi(msg_re.group('from'))
			if fromMbox == -1:
				fromName = 'unknown'
			elif fromMbox == 0:
				fromName = 'operator'
			else:
				fromName = `fromMbox`
			length = os.stat(os.path.join(self.messageDir, fn))[ST_SIZE]
			self.messageList.insert(END,
				'%2d: %02d-%02d-%02d %02d:%02d:%02d %u sec from %s\n'
					% (count, year, mon, day, hour, min, sec,
						length * 8.0 / RATE, fromName))


	def playMessage(self, event=None):
		"Play the message that was double-clicked"
		item = self.messageList.curselection()
		if item:
			itemNum = atoi(item[0])
			os.system((PLAY_CMD + ' &')
						% os.path.join(self.messageDir, self.messageFiles[itemNum]))

	def delMessage(self):
		"Delete the current message"
		item = self.messageList.curselection()
		if item:
			itemNum = atoi(item[0])
			fn = os.path.join(self.messageDir, self.messageFiles[itemNum])
			os.unlink(fn)
			self.refreshDir()

	def arcMessage(self):
		"Move the current message into the archive directory"
		if not fileExists(self.arcDir) or \
		   not S_ISDIR(os.stat(self.arcDir)[ST_MODE]):
			# Message directory doesn't exist -- try to create it
			try:
				os.mkdir(self.arcDir)
				os.chmod(self.arcDir, ARC_DIR_PERM)
			except os.error:
				print 'Cannot create archive directory',self.arcDir
				return

		item = self.messageList.curselection()
		if item:
			itemNum = atoi(item[0])
			fn = os.path.join(self.messageDir, self.messageFiles[itemNum])
			newfn = os.path.join(self.arcDir, self.messageFiles[itemNum])

			if fileExists(newfn):
				return	# file already exists

			os.system('mv %s %s' % (fn, newfn))

			self.refreshDir()

	def switchIncoming(self):
		"Switch to the incoming message directory"
		self.messageDir = self.incomingDir
		self.refreshDir()

		self.scanbutton['command'] = self.switchArchive
		self.scanbutton['text'] = self.scanbutton.viewArcMsg
		self.arcbutton['state'] = NORMAL
		self.popup.entryconfigure(self.arcPopupEntry, state=NORMAL)

	def switchArchive(self):
		"Switch to the archived message directory"
		self.messageDir = self.arcDir
		self.refreshDir()

		self.scanbutton['command'] = self.switchIncoming
		self.scanbutton['text'] = self.scanbutton.viewIncMsg
		self.arcbutton['state'] = DISABLED
		self.popup.entryconfigure(self.arcPopupEntry, state=DISABLED)

def mvmmessage(vmbox):
	'''Start mvmmessage on the given mailbox number.
	If the mailbox number is empty, try mailbox 0, then the incoming directory.
	'''
	if not vmbox:
		vmbox = DEF_MAILBOX 	# default mailbox number
	mbox_dir = os.path.join(os.path.join(getSpoolDir(), MAILBOX_DIR), vmbox)
	if not fileExists(mbox_dir) or not S_ISDIR(os.stat(mbox_dir)[ST_MODE]):

		# Given mailbox doesn't exist; look for alternate directory
		vmbox = ''
		mbox_dir = os.path.join(getSpoolDir(), ALT_MAILBOX_DIR)
		if not fileExists(mbox_dir) or not S_ISDIR(os.stat(mbox_dir)[ST_MODE]):
			printerr('Error: cannot find voice mail directory')
			exit(1)

	msgwin = MvmMessages()
	title = 'Minimal Voice Mail Messages'
	if vmbox:
		title = title + ': Mailbox '+vmbox
	msgwin.master.title(title)
	msgwin.master.iconname('Voice Mailbox '+vmbox)
	# This refreshes the display every time the window comes into focus, but
	# can be sluggish if focus follows the cursor
	#msgwin.master.protocol("WM_TAKE_FOCUS", msgwin.refreshDir)

	# Unfortunately, this doesn't work
	#icon = Image('bitmap')
	#icon['data'] = icondata
	#msgwin.master.iconbitmap(icon)

	# Have to write the icon to a file in order to use it as the icon
	tmpicon = open(tempfile.mktemp(),'w')
	tmpicon.write(icondata)
	tmpicon.close()
	msgwin.master.iconbitmap('@'+tmpicon.name)
	os.unlink(tmpicon.name)

	arc_dir = os.path.join(mbox_dir, ARCHIVE_DIR)
	try:
		msgwin.scanMessages(mbox_dir, arc_dir)
	except os.error:
		printerr("Error: don't have permission to read from"+mbox_dir)
		exit(1)
	msgwin.mainloop()

if __name__=='__main__':
	# First command-line argument is optional mailbox number
	if len(argv) > 1:
		box = argv[1]
	else:
		box = ''
	mvmmessage(box)
