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.1pre4
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 ABOOKFILE=os.environ["HOME"] + "/.abook.addressbook"
57 testcurses = curses.KEY_UP
59 print "Your pythoncurses module is old. Please upgrade to 1.4 or higher."
60 print "Alternative: Modify the script. Change all 6 occurences of curses.KEY_UP"
61 print "and similiar to lowercase."
63 try : testAF = ABOOKFILE #test if ALIASFILE was configured in script
65 try: ALIASFILE=os.environ["ABOOKFILE"] #test is environment MUTTALIASFILE was set
67 print "Please specify ABOOKFILE at beginning of script \nor set environment variable ABOOKFILE"
73 ###Thanks for the following go to Michael P. Reilly
74 if not sys.stdin.isatty(): # is it not attached to a terminal?
75 file = sys.stdin.read()
76 sys.stdin = _dev_tty = open('/dev/tty', 'w+')
77 # close the file descriptor for stdin thru the posix module
79 # now we duplicate the opened _dev_tty file at file descriptor 0 (fd0)
80 # really, dup creates the duplicate at the lowest numbered, closed file
81 # descriptor, but since we just closed fd0, we know we will dup to fd0
82 os.dup(_dev_tty.fileno()) # on UNIX, the fileno() method returns the
83 # file object's file descriptor
85 print "Please use as a pipe!"
88 # now standard input points to the terminal again, at the C level, not just
89 # at the Python level.
92 print "Looking for mail adresses, this may take a while..."
99 "Class to create a simple to use menu using curses"
101 import curses, traceback
105 self.stdscr=curses.initscr()
106 # Turn off echoing of keys, and enter cbreak mode,
107 # where no buffering is performed on keyboard input
111 # In keypad mode, escape sequences for special keys
112 # (like the cursor keys) will be interpreted and
113 # a special value like curses.KEY_LEFT will be returned
114 self.stdscr.keypad(1)
116 def titel(self,TITEL="Title - test",HELP="Use up and down arrows + Return"):
117 "Draws Title and Instructions"
118 self.stdscr.addstr(0,0,TITEL,curses.A_UNDERLINE) # Title + Help
119 self.stdscr.addstr(self.Y -2 ,0,HELP,curses.A_REVERSE)
122 self.stdscr.refresh()
125 "Returns screen size and cursor position"
127 self.Y, self.X = self.stdscr.getmaxyx()
128 #self.y, self.x = 0, 0
129 self.y, self.x = self.stdscr.getyx()
131 return self.Y, self.X, self.y, self.x
134 def showlist(self,LISTE,LSECT=1):
135 "Analyzes list, calculates screen, draws current part of list on screen "
136 s = self.Y -3 #space on screen
138 while len(LISTE) > self.MAXSECT * s : # how many times do we need the screen?
139 self.MAXSECT = self.MAXSECT +1
141 if self.LSECT > self.MAXSECT: #check for end of list
142 self.LSECT = LSECT -1
144 if self.LSECT <= 0: #
151 self.LISTPART=LISTE[s * ( self.LSECT -1 ) : s * self.LSECT ] # part of the List is shown
153 self.stdscr.addstr(self.Y -2, self.X -len(`self.LSECT`+`self.MAXSECT`) -5, "(" + `self.LSECT` + "/" + `self.MAXSECT` + ")")
154 #if len(LISTE) > self.Y - 3:
156 for i in range (1, self.Y -2): # clear screen between title and help text
157 self.stdscr.move(i , 0)
158 self.stdscr.clrtoeol()
159 for i in range (0,len(self.LISTPART)): # print out current part of list
160 Item = self.LISTPART[i]
161 self.stdscr.addstr(i +1, 0, Item[:self.X])
163 def getresult(self,HOEHE):
164 "Get Result from cursor position"
165 RESULT= self.LISTPART[(HOEHE -1)]
168 def showresult(self, HOEHE, DICT={}):
169 "Look up result to show in dictionary if provided, return list member otherwise"
171 return self.getresult(HOEHE)
173 return string.join(DICT[self.getresult(HOEHE)], " || ")
177 def menucall(self, LISTE, DICT={}, TITEL="",HELP="Use up and down arrows, Return to select"):
178 "Takes a list and offers a menu where you can choose one item, optionally, look up result in dictionary if provided"
182 self.titel(TITEL,HELP)
185 self.stdscr.move(1,0)
186 while 1: # read Key events
187 c = self.stdscr.getch()
190 #if c == curses.KEY_LEFT and self.x > 0:
191 # self.stdscr.move(self.y, self.x -1); REFY = 1 # REFY == refresh: yes
193 #if c == curses.KEY_RIGHT and self.x < self.X -1:
194 # #if x < 4 and LENGTH - ZAHLY > y - 1:
195 # self.stdscr.move(self.y, self.x + 1); REFY = 1
197 if c == curses.KEY_UP or c == 107: #up arrow or k
199 self.stdscr.move(self.y -1, self.x); REFY = 1
201 self.LSECT=self.LSECT-1
202 self.showlist(LISTE,self.LSECT)
203 self.stdscr.move(len(self.LISTPART), 0)
206 if c == curses.KEY_DOWN or c == 106: #down arrow or j
208 if self.y < len(self.LISTPART) :
209 self.stdscr.move(self.y +1, self.x); REFY = 1
212 self.LSECT=self.LSECT+1
213 self.showlist(LISTE,self.LSECT)
214 self.stdscr.move(1,0)
217 if c == curses.KEY_PPAGE:
218 self.LSECT=self.LSECT-1
219 self.showlist(LISTE,self.LSECT)
220 self.stdscr.move(1, 0)
223 if c == curses.KEY_NPAGE:
224 self.LSECT=self.LSECT+1
225 self.showlist(LISTE,self.LSECT)
226 self.stdscr.move(1,0)
229 if c == curses.KEY_END:
230 self.LSECT=self.MAXSECT
231 self.showlist(LISTE,self.LSECT)
232 self.stdscr.move(1,0)
235 if c == curses.KEY_HOME:
237 self.showlist(LISTE,self.LSECT)
238 self.stdscr.move(1,0)
242 if c == 10 : # \n (new line)
243 ERG = self.getresult(self.y )
247 if c == 113 or c == 81: # "q or Q"
248 self.printoutnwait("Aborted by user!")
256 self.stdscr.move(self.Y -1, 0)
257 self.stdscr.clrtoeol()
258 self.stdscr.addstr(self.Y -1, 0, self.showresult(self.y,DICT)[:self.X -1 ], curses.A_BOLD)
259 self.stdscr.move(self.y, self.x)
264 # In the event of an error, restore the terminal
266 self.Y, self.X, self.y, self.x = 0, 0, 0, 0
268 self.stdscr.keypad(0)
272 #traceback.print_exc()
274 def input(self, promptstr):
275 "raw_input equivalent in curses, asks for Input and returns it"
278 self.stdscr.move(0,0)
280 self.stdscr.addstr(promptstr)
282 INPUT=self.stdscr.getstr()
287 def printoutnwait(self, promptstr):
288 "Print out Text, wait for key"
290 self.stdscr.move(0,0)
292 # The new Mutt client pauses after running the script already. No reason
293 # to pause twice. -Josh
294 # self.stdscr.addstr(promptstr+"\n\n(press key)")
296 # c = self.stdscr.getch()# read Key events
300 def listrex (str, rgx): # Return a list of all regexes matched in string
301 "Search string for all occurences of regex and return a list of them."
303 start = 0 # set counter to zero
304 ende =len (str) #set end position
305 suchadress = re.compile(rgx,re.LOCALE)#compile regular expression, with LOCALE
307 einzelerg = suchadress.search(str, start,ende) #create Match Object einzelerg
308 if einzelerg == None:#until none is found
310 result.append(einzelerg.group()) #add to result
311 start = einzelerg.end()
314 def strrex (str): # Return first occurence of regular exp as string
315 "Search string for first occurence of regular expression and return it"
316 muster = re.compile(r"<?[\w\b.ßüöä-]+\@[\w.-]+>?", re.LOCALE) #compile re
317 matobj = muster.search(str) #get Match Objekt from string
318 if muster.search(str) == None: #if none found
320 return matobj.group() #return string
322 def stripempty (str):#Looks for all empty charcters and replaces them with a space
323 "Looks for all empty charcters and replaces them with a space,kills trailing"
324 p = re.compile( "\s+") #shorten
325 shrt = p.sub(" ", str)
326 q = re.compile("^\s+|\s+$") #strip
327 strp = q.sub("", shrt)
330 def getmailadressfromstring(str):
331 "Takes str and gets the first word containing @ == mail adress"
332 StringSplit=string.split(str)
333 for i in range(len(StringSplit)):
334 if "@" in StringSplit[i]:
335 return StringSplit[i]
340 OCCLIST = listrex(file, '"?[\s\w\ö\ä\ü\-\ß\_.]*"?\s*<?[\w.-]+\@[\w.-]+>?')#get list, RE gets all Email adresses + prepending words
343 print len(OCCLIST),"possible adresses found!."
345 print"ERROR, no mails found"
349 for i in range(len(OCCLIST)): #strip all whitespaces + trailing from each list member
350 OCCLIST[i] = string.strip(OCCLIST [i])
353 OCCDIC={} # Dictionary created to avoid duplicates
354 for i in range(len(OCCLIST)): # iterate over
356 Mail = getmailadressfromstring(OCCLIST[i])
357 #strrex(OCCLIST[i]) #Mailadresse
358 Schnitt = - len(Mail) #cut off mail adress
359 Mail = string.replace(Mail, "<", "")#remove <>
360 Mail = string.replace(Mail, ">", "")
361 Name = string.replace (stripempty (d[:Schnitt]), '"', '') #leaves name
362 if not OCCDIC.get(Mail): # if new Emailadress
363 Liste = [] # create list for names
364 Liste.append(Name) # append name
365 OCCDIC[Mail] = Liste # assign list to adress
367 Liste = OCCDIC[Mail] # otherwise get list
368 Liste.append(Name) # append name to list of names
369 OCCDIC[Mail] = Liste # and assign
372 KEYS = OCCDIC.keys() #iterate over dictionary, sort names
373 #KEYS are all the adresses
375 for z in range( len(KEYS) ):
376 NAMLIST = OCCDIC[KEYS[z]] # Get list of possible names
377 d = {} # sort out duplicates and
378 # remove bad names + empty strings from adresses
380 if x in ["", "<"]: continue
383 NAMLIST.sort() # sort namelist alphabetically
384 print z, KEYS[z], "had possible names:", NAMLIST # Debugging output
385 OCCDIC[KEYS[z]] = NAMLIST #
391 def Comparelength(x, y):
392 "Compare number of names in OCCDIC, if equal sort alphabetically."
393 if len(OCCDIC[y]) == len(OCCDIC[x]):
395 if len(OCCDIC[y]) < len(OCCDIC[x]):
400 KEYS.sort(Comparelength) # Keys sort
404 ScreenObject=screenC() # initialize curses menu
406 ZIELADRESS = ScreenObject.menucall(KEYS, OCCDIC, "Choose adress to alias")
407 if OCCDIC[ZIELADRESS]:
408 LISTNAM=["***ENTER own NAME"] #add option to edit name
409 LISTNAM= LISTNAM + OCCDIC[ZIELADRESS]
410 ZIELNAME = ScreenObject.menucall(LISTNAM, {}, ZIELADRESS + " has which of the possible names?")
411 # empty Dictionary {} means show list member itself, not looked up result
414 T=ScreenObject.size()
416 # traceback.print_exc() # Uncomment for curses debugging info
420 ### enter new names/aliases
422 if ZIELNAME == "***ENTER own NAME" or ZIELNAME == "":
423 ZIELNAME = ScreenObject.input(`ZIELADRESS` + " = " + `OCCDIC[ZIELADRESS]` + "\n" + `ZIELADRESS` + " gets which name? ")
428 WRITEALIAS = "\n[]\nname=" + ZIELNAME + "\nemail=" + ZIELADRESS + "\n\n"
430 f = open(ABOOKFILE, "a")
434 ScreenObject.printoutnwait("Item was added to "+ ABOOKFILE + "\nProgam terminated")