3 # NOTE: starting from abook version 0.4.15 it is often more convenient
4 # to use command line option "--add-email" instead of this script
6 # mail2abook.py version 0.1
7 # by Jaakko Heinonen <jheinonen@users.sourceforge.net>
8 # based on Moritz Moeller-Herrmann's mail2muttalias.py
11 # this script reads all mail adresses from a piped mailfile and
12 # offers to write them to the ABOOKFILE specified in the script
13 # Append the following lines or similiar to your muttrc:
14 # macro pager A |'path to script'/mail2abook.py\n
16 # Then press A in pager mode to add an address to your abook addressbook
18 # Here's the original copyright information:
22 # Needs python 1.5 and cursesmodule 1.4 by andrich@fga.de (Oliver Andrich)
23 # available somewhere on http://www.python.org or directly from
24 # http://andrich.net/python/selfmade.html
26 # Copyright by Moritz Moeller-Herrmann <mmh@gmnx.net>
28 # Homepage (check for changes before reporting a bug)
29 # http://webrum.uni-mannheim.de/jura/moritz/mail2muttalias.html
31 # Use this any way you want. Please inform me of any error/improvement
32 # and leave the copyright notice untouched.
33 # No warranties, as this is my first program :-)
34 # Works for me on Linux with Python 1.5+
36 # Thanks to Josh Hildebrand for some improvements
40 # If name containing @ is quoted("") before a mail adress,
41 # the program will not catch the whole name,
42 # but who would alias such an idiot?
43 # If you get a problem with "unknown variable / keyword error"
44 # with KEYUP and KEYDOWN, either get a newer pythoncurses module
45 # or change them to lowercase.
47 # Probably some more, mail me if you find one!
50 import string, sys, os
51 import curses, traceback
54 locale.setlocale(locale.LC_ALL,os.environ["LANG"])
56 ABOOKFILE=os.environ["HOME"] + "/.abook/addressbook"
59 testcurses = curses.KEY_UP
61 print "Your pythoncurses module is old. Please upgrade to 1.4 or higher."
62 print "Alternative: Modify the script. Change all 6 occurences of curses.KEY_UP"
63 print "and similiar to lowercase."
65 try : testAF = ABOOKFILE #test if ALIASFILE was configured in script
67 try: ALIASFILE=os.environ["ABOOKFILE"] #test is environment MUTTALIASFILE was set
69 print "Please specify ABOOKFILE at beginning of script \nor set environment variable ABOOKFILE"
75 ###Thanks for the following go to Michael P. Reilly
76 if not sys.stdin.isatty(): # is it not attached to a terminal?
77 file = sys.stdin.read()
78 sys.stdin = _dev_tty = open('/dev/tty', 'w+')
79 # close the file descriptor for stdin thru the posix module
81 # now we duplicate the opened _dev_tty file at file descriptor 0 (fd0)
82 # really, dup creates the duplicate at the lowest numbered, closed file
83 # descriptor, but since we just closed fd0, we know we will dup to fd0
84 os.dup(_dev_tty.fileno()) # on UNIX, the fileno() method returns the
85 # file object's file descriptor
87 print "Please use as a pipe!"
90 # now standard input points to the terminal again, at the C level, not just
91 # at the Python level.
94 print "Looking for mail adresses, this may take a while..."
101 "Class to create a simple to use menu using curses"
103 import curses, traceback
107 self.stdscr=curses.initscr()
108 # Turn off echoing of keys, and enter cbreak mode,
109 # where no buffering is performed on keyboard input
113 # In keypad mode, escape sequences for special keys
114 # (like the cursor keys) will be interpreted and
115 # a special value like curses.KEY_LEFT will be returned
116 self.stdscr.keypad(1)
118 def titel(self,TITEL="Title - test",HELP="Use up and down arrows + Return"):
119 "Draws Title and Instructions"
120 self.stdscr.addstr(0,0,TITEL,curses.A_UNDERLINE) # Title + Help
121 self.stdscr.addstr(self.Y -2 ,0,HELP,curses.A_REVERSE)
124 self.stdscr.refresh()
127 "Returns screen size and cursor position"
129 self.Y, self.X = self.stdscr.getmaxyx()
130 #self.y, self.x = 0, 0
131 self.y, self.x = self.stdscr.getyx()
133 return self.Y, self.X, self.y, self.x
136 def showlist(self,LISTE,LSECT=1):
137 "Analyzes list, calculates screen, draws current part of list on screen "
138 s = self.Y -3 #space on screen
140 while len(LISTE) > self.MAXSECT * s : # how many times do we need the screen?
141 self.MAXSECT = self.MAXSECT +1
143 if self.LSECT > self.MAXSECT: #check for end of list
144 self.LSECT = LSECT -1
146 if self.LSECT <= 0: #
153 self.LISTPART=LISTE[s * ( self.LSECT -1 ) : s * self.LSECT ] # part of the List is shown
155 self.stdscr.addstr(self.Y -2, self.X -len(`self.LSECT`+`self.MAXSECT`) -5, "(" + `self.LSECT` + "/" + `self.MAXSECT` + ")")
156 #if len(LISTE) > self.Y - 3:
158 for i in range (1, self.Y -2): # clear screen between title and help text
159 self.stdscr.move(i , 0)
160 self.stdscr.clrtoeol()
161 for i in range (0,len(self.LISTPART)): # print out current part of list
162 Item = self.LISTPART[i]
163 self.stdscr.addstr(i +1, 0, Item[:self.X])
165 def getresult(self,HOEHE):
166 "Get Result from cursor position"
167 RESULT= self.LISTPART[(HOEHE -1)]
170 def showresult(self, HOEHE, DICT={}):
171 "Look up result to show in dictionary if provided, return list member otherwise"
173 return self.getresult(HOEHE)
175 return string.join(DICT[self.getresult(HOEHE)], " || ")
179 def menucall(self, LISTE, DICT={}, TITEL="",HELP="Use up and down arrows, Return to select"):
180 "Takes a list and offers a menu where you can choose one item, optionally, look up result in dictionary if provided"
184 self.titel(TITEL,HELP)
187 self.stdscr.move(1,0)
188 while 1: # read Key events
189 c = self.stdscr.getch()
192 #if c == curses.KEY_LEFT and self.x > 0:
193 # self.stdscr.move(self.y, self.x -1); REFY = 1 # REFY == refresh: yes
195 #if c == curses.KEY_RIGHT and self.x < self.X -1:
196 # #if x < 4 and LENGTH - ZAHLY > y - 1:
197 # self.stdscr.move(self.y, self.x + 1); REFY = 1
199 if c == curses.KEY_UP or c == 107: #up arrow or k
201 self.stdscr.move(self.y -1, self.x); REFY = 1
203 self.LSECT=self.LSECT-1
204 self.showlist(LISTE,self.LSECT)
205 self.stdscr.move(len(self.LISTPART), 0)
208 if c == curses.KEY_DOWN or c == 106: #down arrow or j
210 if self.y < len(self.LISTPART) :
211 self.stdscr.move(self.y +1, self.x); REFY = 1
214 self.LSECT=self.LSECT+1
215 self.showlist(LISTE,self.LSECT)
216 self.stdscr.move(1,0)
219 if c == curses.KEY_PPAGE:
220 self.LSECT=self.LSECT-1
221 self.showlist(LISTE,self.LSECT)
222 self.stdscr.move(1, 0)
225 if c == curses.KEY_NPAGE:
226 self.LSECT=self.LSECT+1
227 self.showlist(LISTE,self.LSECT)
228 self.stdscr.move(1,0)
231 if c == curses.KEY_END:
232 self.LSECT=self.MAXSECT
233 self.showlist(LISTE,self.LSECT)
234 self.stdscr.move(1,0)
237 if c == curses.KEY_HOME:
239 self.showlist(LISTE,self.LSECT)
240 self.stdscr.move(1,0)
244 if c == 10 : # \n (new line)
245 ERG = self.getresult(self.y )
249 if c == 113 or c == 81: # "q or Q"
250 self.printoutnwait("Aborted by user!")
258 self.stdscr.move(self.Y -1, 0)
259 self.stdscr.clrtoeol()
260 self.stdscr.addstr(self.Y -1, 0, self.showresult(self.y,DICT)[:self.X -1 ], curses.A_BOLD)
261 self.stdscr.move(self.y, self.x)
266 # In the event of an error, restore the terminal
268 self.Y, self.X, self.y, self.x = 0, 0, 0, 0
270 self.stdscr.keypad(0)
274 #traceback.print_exc()
276 def input(self, promptstr):
277 "raw_input equivalent in curses, asks for Input and returns it"
280 self.stdscr.move(0,0)
282 self.stdscr.addstr(promptstr)
284 INPUT=self.stdscr.getstr()
289 def printoutnwait(self, promptstr):
290 "Print out Text, wait for key"
292 self.stdscr.move(0,0)
294 # The new Mutt client pauses after running the script already. No reason
295 # to pause twice. -Josh
296 # self.stdscr.addstr(promptstr+"\n\n(press key)")
298 # c = self.stdscr.getch()# read Key events
302 def listrex (str, rgx): # Return a list of all regexes matched in string
303 "Search string for all occurences of regex and return a list of them."
305 start = 0 # set counter to zero
306 ende =len (str) #set end position
307 suchadress = re.compile(rgx,re.LOCALE)#compile regular expression, with LOCALE
309 einzelerg = suchadress.search(str, start,ende) #create Match Object einzelerg
310 if einzelerg == None:#until none is found
312 result.append(einzelerg.group()) #add to result
313 start = einzelerg.end()
316 def strrex (str): # Return first occurence of regular exp as string
317 "Search string for first occurence of regular expression and return it"
318 muster = re.compile(r"<?[\w\b.ßüöä-]+\@[\w.-]+>?", re.LOCALE) #compile re
319 matobj = muster.search(str) #get Match Objekt from string
320 if muster.search(str) == None: #if none found
322 return matobj.group() #return string
324 def stripempty (str):#Looks for all empty charcters and replaces them with a space
325 "Looks for all empty charcters and replaces them with a space,kills trailing"
326 p = re.compile( "\s+") #shorten
327 shrt = p.sub(" ", str)
328 q = re.compile("^\s+|\s+$") #strip
329 strp = q.sub("", shrt)
332 def getmailadressfromstring(str):
333 "Takes str and gets the first word containing @ == mail adress"
334 StringSplit=string.split(str)
335 for i in range(len(StringSplit)):
336 if "@" in StringSplit[i]:
337 return StringSplit[i]
342 OCCLIST = listrex(file, '"?[\s\w\ö\ä\ü\-\ß\_.]*"?\s*<?[\w.-]+\@[\w.-]+>?')#get list, RE gets all Email adresses + prepending words
345 print len(OCCLIST),"possible adresses found!."
347 print"ERROR, no mails found"
351 for i in range(len(OCCLIST)): #strip all whitespaces + trailing from each list member
352 OCCLIST[i] = string.strip(OCCLIST [i])
355 OCCDIC={} # Dictionary created to avoid duplicates
356 for i in range(len(OCCLIST)): # iterate over
358 Mail = getmailadressfromstring(OCCLIST[i])
359 #strrex(OCCLIST[i]) #Mailadresse
360 Schnitt = - len(Mail) #cut off mail adress
361 Mail = string.replace(Mail, "<", "")#remove <>
362 Mail = string.replace(Mail, ">", "")
363 Name = string.replace (stripempty (d[:Schnitt]), '"', '') #leaves name
364 if not OCCDIC.get(Mail): # if new Emailadress
365 Liste = [] # create list for names
366 Liste.append(Name) # append name
367 OCCDIC[Mail] = Liste # assign list to adress
369 Liste = OCCDIC[Mail] # otherwise get list
370 Liste.append(Name) # append name to list of names
371 OCCDIC[Mail] = Liste # and assign
374 KEYS = OCCDIC.keys() #iterate over dictionary, sort names
375 #KEYS are all the adresses
377 for z in range( len(KEYS) ):
378 NAMLIST = OCCDIC[KEYS[z]] # Get list of possible names
379 d = {} # sort out duplicates and
380 # remove bad names + empty strings from adresses
382 if x in ["", "<"]: continue
385 NAMLIST.sort() # sort namelist alphabetically
386 print z, KEYS[z], "had possible names:", NAMLIST # Debugging output
387 OCCDIC[KEYS[z]] = NAMLIST #
393 def Comparelength(x, y):
394 "Compare number of names in OCCDIC, if equal sort alphabetically."
395 if len(OCCDIC[y]) == len(OCCDIC[x]):
397 if len(OCCDIC[y]) < len(OCCDIC[x]):
402 KEYS.sort(Comparelength) # Keys sort
406 ScreenObject=screenC() # initialize curses menu
408 ZIELADRESS = ScreenObject.menucall(KEYS, OCCDIC, "Choose adress to alias")
409 if OCCDIC[ZIELADRESS]:
410 LISTNAM=["***ENTER own NAME"] #add option to edit name
411 LISTNAM= LISTNAM + OCCDIC[ZIELADRESS]
412 ZIELNAME = ScreenObject.menucall(LISTNAM, {}, ZIELADRESS + " has which of the possible names?")
413 # empty Dictionary {} means show list member itself, not looked up result
416 T=ScreenObject.size()
418 # traceback.print_exc() # Uncomment for curses debugging info
422 ### enter new names/aliases
424 if ZIELNAME == "***ENTER own NAME" or ZIELNAME == "":
425 ZIELNAME = ScreenObject.input(`ZIELADRESS` + " = " + `OCCDIC[ZIELADRESS]` + "\n" + `ZIELADRESS` + " gets which name? ")
430 WRITEALIAS = "\n[]\nname=" + ZIELNAME + "\nemail=" + ZIELADRESS + "\n\n"
432 f = open(ABOOKFILE, "a")
436 ScreenObject.printoutnwait("Item was added to "+ ABOOKFILE + "\nProgam terminated")