root/mhi.py

Revision 63ca54f8c7e93cd7200aa325197a73ce5c76a40d, 25.2 kB (checked in by Paul Jimenez <pj@zachs.org>, 7 months ago)

added pager support
and mark-read support
and folder prefix support
and fixed some bugs

  • Property mode set to 100755
Line 
1 #!/usr/bin/python2.4
2 #
3 # Goal: MH-ish commands that will talk to an IMAP server
4 #
5 # Commands that work: folder, folders, scan, rmm, rmf, pick/search, help,
6 #                     debug, refile, show, next, prev
7 #
8 # Commands to make work: sort, comp, repl, dist, forw, anno, mr
9 #
10 # * sort should just store a sort order to apply to output instead of
11 #   actually touching the mailboxes.  This will affect the working of
12 #   anything that takes a msgset as well as scan, next, prev, and pick
13 #
14 # * mr should do a 'mark all read' on the current (or specified) folder
15 #
16 # handy aliases:
17 #    mailchk - folders with new messages - mhi folders |grep -v " 0$"
18 #    nn - show new messages in a folder- mhi scan `mhi pick \Unseen`
19 #
20 # minor bits of code taken from http://www.w3.org/2000/04/maillog2rdf/imap_sort.py
21 # everything else Copyright Paul Jimenez, released under the GPL
22 # canonical copy at http://www.place.org/~pj/software/mhi
23
24
25 import os
26 import sys
27 import time
28 import string
29 import os.path
30 import imaplib
31 import StringIO
32 from configobj import ConfigObj
33
34 cfgdir = os.environ.get('HOME','')
35 config = ConfigObj(infile="%s/.mhirc" % cfgdir, create_empty=True)
36 state = ConfigObj(infile="%s/.mhistate" % cfgdir, create_empty=True)
37 Debug = 0
38
39 def _debug(dstr):
40     if Debug > 0:
41         print "DEBUG: %s" % dstr
42
43 def sexpr_readsexpr(s):
44     import sexpr
45     return sexpr.SexprParser(StringIO.StringIO(s)).parse()   
46
47 def readlisp_readsexpr(s):
48     import readlisp
49     return readlisp.readlisp(s)
50
51 readsexpr = readlisp_readsexpr
52
53 PickDocs = """ From RFC2060:
54       When multiple keys are specified, the result is the intersection
55       (AND function) of all the messages that match those keys.  For
56       example, the criteria DELETED FROM "SMITH" SINCE 1-Feb-1994 refers
57       to all deleted messages from Smith that were placed in the mailbox
58       since February 1, 1994.  A search key can also be a parenthesized
59       list of one or more search keys (e.g. for use with the OR and NOT
60       keys).
61
62       In all search keys that use strings, a message matches the key if
63       the string is a substring of the field.  The matching is case-
64       insensitive.
65
66       The defined search keys are as follows.  Refer to the Formal
67       Syntax section for the precise syntactic definitions of the
68       arguments.
69
70       <message set>  Messages with message sequence numbers
71                      corresponding to the specified message sequence
72                      number set
73
74       ALL            All messages in the mailbox; the default initial
75                      key for ANDing.
76
77       ANSWERED       Messages with the \Answered flag set.
78
79       BCC <string>   Messages that contain the specified string in the
80                      envelope structure's BCC field.
81
82       BEFORE <date>  Messages whose internal date is earlier than the
83                      specified date.
84
85       BODY <string>  Messages that contain the specified string in the
86                      body of the message.
87
88       CC <string>    Messages that contain the specified string in the
89                      envelope structure's CC field.
90
91       DELETED        Messages with the \Deleted flag set.
92
93       DRAFT          Messages with the \Draft flag set.
94
95       FLAGGED        Messages with the \Flagged flag set.
96
97       FROM <string>  Messages that contain the specified string in the
98                      envelope structure's FROM field.
99
100       HEADER <field-name> <string>
101                      Messages that have a header with the specified
102                      field-name (as defined in [RFC-822]) and that
103                      contains the specified string in the [RFC-822]
104                      field-body.
105
106       KEYWORD <flag> Messages with the specified keyword set.
107
108       LARGER <n>     Messages with an [RFC-822] size larger than the
109                      specified number of octets.
110
111       NEW            Messages that have the \Recent flag set but not the
112                      \Seen flag.  This is functionally equivalent to
113                      "(RECENT UNSEEN)".
114
115       NOT <search-key>
116                      Messages that do not match the specified search
117                      key.
118
119       OLD            Messages that do not have the \Recent flag set.
120                      This is functionally equivalent to "NOT RECENT" (as
121                      opposed to "NOT NEW").
122
123       ON <date>      Messages whose internal date is within the
124                      specified date.
125
126       OR <search-key1> <search-key2>
127                      Messages that match either search key.
128
129       RECENT         Messages that have the \Recent flag set.
130
131       SEEN           Messages that have the \Seen flag set.
132
133       SENTBEFORE <date>
134                      Messages whose [RFC-822] Date: header is earlier
135                      than the specified date.
136
137       SENTON <date>  Messages whose [RFC-822] Date: header is within the
138                      specified date.
139
140       SENTSINCE <date>
141                      Messages whose [RFC-822] Date: header is within or
142                      later than the specified date.
143
144       SINCE <date>   Messages whose internal date is within or later
145                      than the specified date.
146
147       SMALLER <n>    Messages with an [RFC-822] size smaller than the
148                      specified number of octets.
149
150       SUBJECT <string>
151                      Messages that contain the specified string in the
152                      envelope structure's SUBJECT field.
153
154       TEXT <string>  Messages that contain the specified string in the
155                      header or body of the message.
156
157       TO <string>    Messages that contain the specified string in the
158                      envelope structure's TO field.
159
160       UID <message set>
161                      Messages with unique identifiers corresponding to
162                      the specified unique identifier set.
163
164       UNANSWERED     Messages that do not have the \Answered flag set.
165
166       UNDELETED      Messages that do not have the \Deleted flag set.
167
168       UNDRAFT        Messages that do not have the \Draft flag set.
169
170       UNFLAGGED      Messages that do not have the \Flagged flag set.
171
172       UNKEYWORD <flag>
173                      Messages that do not have the specified keyword
174                      set.
175
176       UNSEEN         Messages that do not have the \Seen flag set.
177
178 """
179
180 class UsageError:
181     pass
182
183 def _argFolder(args, default=None):
184     '''
185     parse the args into a folder-spec (denoted by a leading +, last of
186     which is used if multiple are listed), and the rest of the args
187     '''
188     folder = None
189     outargs = []
190     for a in args:
191         if a.startswith('+') and len(a) > 1:
192             folder = a[1:]
193         else:
194             outargs.append(a)
195     if folder is None:
196         folder = default
197     else:
198         prefix = config.get('folder_prefix','')
199         folder = prefix + folder
200     return folder, outargs
201
202 def _connect():
203     ''' Convenience connection creation function '''
204     import urlparse
205     schemes = { 'imap' : imaplib.IMAP4,
206                 'imaps': imaplib.IMAP4_SSL,
207                 'stream': imaplib.IMAP4_stream }
208     scheme, netloc, path, _, _, _ = urlparse.urlparse(config['connection'])
209     _debug('scheme: %s netloc: %s path: %s' % (scheme, netloc, path))
210     if netloc:
211         if '@' in netloc:
212             userpass, hostport = netloc.rsplit('@', 1)
213         else:
214             userpass, hostport = netloc, 'localhost'
215         if ':' in hostport:
216             host, port = hostport.rsplit(':', 1)
217         else:
218             host = hostport
219             port = "143"
220             if scheme[-1] == 's':
221                 port = "993"
222         if ':' in userpass:
223             user, passwd = userpass.split(':', 1)
224         else:
225             user, passwd = os.environ.get('USER',''), userpass
226         _debug("%s connection to %s : %s @ %s : %s" % (scheme, user, passwd, host, port))
227         session = schemes[scheme](host, int(port))
228         session.login(user, passwd)
229     else:
230         session = schemes[scheme](path)
231     session.debug = Debug
232     return session
233
234 ''' Convenience exit-on-error wrapper '''
235 def do_or_die(func, msgstr):
236     result, data = func
237     if result != 'OK':
238         print msgstr+' ' + str(data)
239         sys.exit(1)
240     return data
241
242 ''' Change some common symbols into an IMAP-style msgset '''
243 def _fixupMsgset(msgset):
244     # s/cur/$cur/, s/last/$last/, s/prev/$prev/, s/next/$next/
245     cur = state.get(state['folder']+'.cur', None)
246     if cur == 'None': cur = None
247     if cur is not None:
248         #print "DEBUG: cur is %s" % repr(cur)
249         #print "DEBUG: type(cur) is %s" % repr(type(cur))
250         msgset = msgset.replace('cur', cur)
251         # XXX: bounds-check these?
252         msgset = msgset.replace('next', str(int(cur)+1))
253         msgset = msgset.replace('prev', str(int(cur)-1))
254     else:
255         requiresCur = False
256         for dep in ["cur", "prev", "next"]:
257             requiresCur = requiresCur or dep in msgset
258         if requiresCur:
259             print "No current message, so '%s' makes no sense." % msgset
260             sys.exit(1)
261     msgset = msgset.replace('-', ':')
262     msgset = msgset.replace(' ', ',')
263     msgset = msgset.replace('last', "*")
264     msgset = msgset.replace('$', "*")
265     return msgset
266
267 '''Stub to check that a specified string has the grammar of a msgset'''
268 def _checkMsgset(msgset):
269     ## FIXME: need a better check that msgset is a valid imap messageset string
270     # msgset = int | int:int | msgset,msgset
271     # '1', '1:5', '1,2,3', '1,3:5' are all valid
272     if len(msgset.strip('1234567890,:*')) != 0:
273         print "%s isn't a valid messageset. Try again." % msgset
274         sys.exit(1)
275
276 def _crlf_terminate(msgfile):
277     ''' convenience function to turn a \n terminated file into a \r\n terminated file '''
278     tfile = os.tempnam()
279     os.rename(msgfile,tfile)
280     inf = file(tfile,"r")
281     outf = file(msgfile,"w")
282     for line in inf:
283         if len(line) >= 2 and line[-2] != '\r' and line[-1] == '\n':
284            line = line[:-1]+'\r\n'
285            outf.write(line)
286     inf.close()
287     outf.close()
288
289 def _edit(msgfile):
290     ''' internal common code for comp/repl/dist/medit '''
291     env = os.environ
292     editor = env.get('VISUAL',env.get('EDITOR', 'editor'))
293     fin = os.system("%s %s" % (editor, msgfile))
294     _crlf_terminate(msgfile)
295     return fin
296
297 def _SMTPsend(msgfile):
298     import smtplib
299     import email
300     ret = {'Unknown':'SMTP problem'}
301     msg = email.message_from_file(file(msgfile,"r"))
302     fromaddr = msg.get('From','')
303     toaddrs = msg.get_all('To',[])
304     server = smtplib.SMTP('localhost')
305     #server.set_debuglevel(1)
306     try:
307         ret = server.sendmail(fromaddr, toaddrs, msg.as_string())
308     except smtplib.SMTPRecipientsRefused:
309         print "No valid recipients. Try again."
310     except smtplib.SMTPHeloError:
311         print "Error talking to SMTP server (No HELO)."
312     except smtplib.SMTPSenderRefused:
313         print "Error talking to SMTP server (Unacceptable FROM address)."
314     except smtplib.SMTPDataError:
315         print "Error talking to SMTP server (Data Error)."
316     server.quit()
317     for k in ret.keys():
318         print "SMTP Error: %s: %s" % (k, ret[k])
319     return len(ret.keys())
320
321
322 def comp(args):
323     '''Usage: comp
324     
325     Compose a new message
326     '''
327     tmpfile = os.tempnam(None,'mhi-comp-')
328     ret = _edit(tmpfile)
329     if ret == 0:
330         # edit succeeded, wasn't aborted or anything
331         errcount = _SMTPsend(tmpfile)
332         if not errcount:
333             os.unlink(tmpfile)
334     else:
335         # 'abort - throw away session, keep - save it for later'
336         print "Session aborted."
337         os.unlink(tmpfile)
338
339
340 def repl(args):
341     '''Usage: repl
342     
343     Reply to the current message, quoting it
344     '''
345     tmpfile = os.tempnam(None,'mhi-repl-')
346     # TODO: put quoted contents of current message into tmpfile
347     ret = _edit(tmpfile)
348     if ret == 0:
349         # edit succeeded, wasn't aborted or anything
350         _SMTPsend(tmpfile)
351         if not errcount:
352             os.unlink(tmpfile)
353     else:
354         # 'abort - throw away session, keep - save it for later'
355         print "Session aborted."
356         os.unlink(tmpfile)
357
358 def _selectOrCreate(S, folder):
359     result, data = S.select(folder)
360     _debug(" Result: %s, %s " % (result, data))
361     if result != 'OK':
362         print "Folder '%s' doesn't exist.  Create it? " % folder,
363         answer = sys.stdin.readline().strip().lower()
364         if answer.startswith('y'):
365             do_or_die(S.create(folder), "Problem creating folder:")
366             do_or_die(S.select(folder), "Problem selecting newly created folder:")
367         else:
368             do_or_die(('',''), "Nothing done. exiting.")
369     return data
370
371 def folder_name(folder):
372     prefix = config.get('folder_prefix',None)
373     if prefix and folder.startswith(prefix):
374         return folder[len(prefix):]
375     return folder
376
377 def folder(args):
378     '''Usage: folder [+<foldername>]
379
380     Change folders / show current folder
381     '''
382     folder, arglist = _argFolder(args, state['folder'])
383     if arglist:
384         raise UsageError()
385     S = _connect()
386     data = _selectOrCreate(S, folder)
387     S.close()
388     S.logout()
389     state['folder'] = folder
390     # inbox+ has 64 messages  (1-64); cur=63; (others).
391     cur = state.get(folder+'.cur', 'unset')
392     print "Folder %s has %s messages, cur is %s." % (folder_name(folder), data[0], cur)
393
394 def folders(args):
395     '''Usage: folders
396     
397     Show all folders
398     '''
399     HEADER = "FOLDER"
400     S = _connect()
401     result, flist = S.list()
402     _debug(" flist: %s " % repr(flist))
403     stats = {}
404     for fline in flist:
405         f = str(readsexpr('('+fline+')')[2])
406         _debug(" f: %s " % repr(f))
407         result, data = S.status(f, '(MESSAGES RECENT UNSEEN)')
408         if result == 'OK':
409             stats[f] = readsexpr('('+data[0]+')')[1]
410     S.logout()
411     stats[HEADER] = [0, "# MESSAGES", 0, "RECENT", 0, "UNSEEN"]
412     folderlist = [ key for key in stats.keys() if key != 'FOLDER' ]
413     folderlist.sort()
414     totalmsgs, totalnew = 0, 0
415     for folder in [HEADER]+folderlist:
416         _debug(" folder: %s " % repr(folder))
417         iscur = [' ', '*'][ folder == state['folder'] ]
418         foo = stats[folder]
419         _debug("  Stats: %s " % repr(foo))
420         messages, recent, unseen = foo[1], foo[3], foo[5]
421         cur = state.get(folder+'.cur', ['-', 'CUR'][ folder == HEADER ])
422         print "%s%-20s %7s %7s %7s %7s" % (iscur, folder_name(folder), cur, messages, recent, unseen)
423         if folder != HEADER:
424             totalmsgs += int(messages)
425             totalnew += int(unseen)
426     print "TOTAL: %d messages (%d new) in %d folders" % (totalmsgs, totalnew, len(folderlist))
427
428
429 def pick(args):
430     '''Usage: pick <search criteria> [+folder]
431
432     Return a message-set that matches the search criteria.
433     Criteria are based on the IMAP spec search string.
434     A summary of the IMAP spec is available by calling 'pick' with --help as its only option.
435     '''
436     if not args:
437         raise UsageError()
438     if len(args) == 1 and args[0] == "--help":
439         print PickDocs
440         sys.exit(1)
441     state['folder'], arglist = _argFolder(args, state['folder'])
442     searchstr = '('+' '.join(arglist)+')'
443     S = _connect()
444     do_or_die(S.select(state['folder']), "Problem changing to folder:")
445     data = do_or_die(S.search(None, searchstr), "Problem with search criteria:")
446     _debug("data: %s" % repr(data))
447     S.close()
448     S.logout()
449     data = [d for d in data if d != '']
450     if data:
451         msglist = []
452         for m in data:
453             msglist += m.split()
454         print ','.join(msglist)
455     else:
456         print "0"
457
458
459 def refile(args):
460     '''Usage: refile <messageset> +<folder>
461
462     Moves a set of messages from the current folder to a new one.
463     '''
464     if not args:
465         raise UsageError()
466     destfolder, arglist = _argFolder(args)
467     if destfolder is None:
468         print "Error: Destination folder must be specified."
469         raise UsageError()
470     srcfolder = state["folder"]
471     msgset = _fixupMsgset(' '.join(arglist))
472     if not msgset:
473         try:
474             msgset = state[srcfolder+".cur"]
475         except KeyError:
476             print "Error: No message(s) selected."
477             raise UsageError()
478     _checkMsgset(msgset)
479     S = _connect()
480     _selectOrCreate(S, destfolder)
481     do_or_die(S.select(srcfolder), "Problem changing folders:")
482     do_or_die(S.copy(msgset, destfolder), "Problem with copy:")
483     data = do_or_die(S.search(None, msgset), "Problem with search:")
484     print "Refiling... ",
485     msgnums = data[0].split()
486     for num in msgnums:
487         S.store(num, '+FLAGS', '\\Deleted')
488         print ".",
489     S.expunge()
490     print "%d messages refiled to '%s'." % (len(msgnums), destfolder)
491     S.close()
492     S.logout()
493     print "Done."
494
495 def rmf(args):
496     '''Usage:  rmf +<foldername>
497     remove a folder
498     '''
499     folder, arglist = _argFolder(args)
500     if not folder:
501         raise UsageError()
502     S = _connect()
503     result, data = S.select(folder)
504     _debug(" Result: %s, %s " % (result, data))
505     if result != 'OK':
506         print "Folder '%s' doesn't exist." % folder
507     else:
508         if state['folder'] == folder:
509             state['folder'] = 'INBOX'
510         do_or_die(S.select(state['folder']), "Problem changing folders:")
511         result, data = S.delete(folder)
512     S.close()
513     S.logout()
514     if result == 'OK':
515         print "Folder '%s' deleted." % folder
516     else:
517         print "Failed to delete folder '%s': %s" % (folder, data)
518
519 def rmm(args):
520     '''Usage: rmm [+folder] <messageset>
521
522     ie: rmm +INBOX 1
523     ie: rmm 1:5
524
525     Remove the specified messages (or the current message if unspecified) 
526     from the specified folder (or the current folder if unspecified).
527     '''
528     folder, arglist = _argFolder(args, state['folder'])
529     state['folder'] = folder
530     msgset = _fixupMsgset(' '.join(arglist))
531     if not msgset:
532         try:
533             msgset = state[folder+'.cur']
534         except KeyError:
535             print "Error: No current message selected."
536             raise UsageError()
537     _checkMsgset(msgset)
538     S = _connect()
539     do_or_die(S.select(folder), "Problem changing folders:")
540     data = do_or_die(S.search(None, msgset), "Problem with search:")
541     do_or_die(S.store(msgset, '+FLAGS', '\\Deleted'), "Problem setting deleted flag: ")
542     do_or_die(S.expunge(), "Problem expunging deleted messages: ")
543     print "Deleted."
544     S.close()
545     S.logout()
546     first = data[0].split()[0]
547     state[folder+'.cur'] = first
548
549 def mr(args):
550     '''Usage: mr [+folder] <messageset>
551
552     Mark the specified messages (or the current message if unspecified)
553     from the specified folder (or the current folder if unspecified) as read.
554     '''
555     folder, arglist = _argFolder(args, state['folder'])
556     state['folder'] = folder
557     msgset = _fixupMsgset(' '.join(arglist))
558     if not msgset:
559         try:
560             msgset = state[folder+'.cur']
561         except KeyError:
562             print "Error: No current message selected."
563             raise UsageError()
564     _checkMsgset(msgset)
565     S = _connect()
566     do_or_die(S.select(folder), "Problem changing folders:")
567     data = do_or_die(S.search(None, msgset), "Problem with search:")
568     do_or_die(S.store(msgset, '+FLAGS', '\\Seen'), "Problem setting read flag: ")
569     S.close()
570     S.logout()
571     first = data[0].split()[0]
572     state[folder+'.cur'] = first
573
574
575
576 def _show(folder, msgset):
577     '''common code for show/next/prev'''
578     S = _connect()
579     do_or_die(S.select(folder), "Problem changing folders:")
580     data = do_or_die(S.search(None, msgset), "Problem with search:")
581     last = None
582     for num in data[0].split():
583         result, data = S.fetch(num, '(RFC822)')
584         print "(Message %s:%s)\n%s\n" % (folder, num, data[0][1])
585         last = num
586     S.close()
587     S.logout()
588     return last
589
590 def show(args):
591     '''Usage:  show [<messageset>]
592
593     Show the specified messages, or the current message if none specified
594     '''
595     folder, arglist = _argFolder(args, state['folder'])
596     state['folder'] = folder
597     msgset = _fixupMsgset(' '.join(arglist))
598     if not msgset:
599         try:
600             msgset = state[folder+'.cur']
601         except KeyError:
602             print "Error: No current message selected."
603             raise UsageError()
604     _checkMsgset(msgset)
605     state[folder+'.cur'] = _show(folder, msgset)
606
607 # TODO: needs better bounds checking
608 def next(args):
609     '''Usage: next [+<folder>]
610
611     Show the next message in the specified folder, or the current folder if not specified
612     '''
613     folder, arglist = _argFolder(args, state['folder'])
614     state['folder'] = folder
615     try:
616         cur = int(state[folder+'.cur']) + 1
617     except KeyError:
618         cur = 1
619     state[folder+'.cur'] = _show(folder, str(cur))
620
621 # TODO: needs better bounds checking
622 def prev(args):
623     '''Usage: prev [+<folder>]
624     Show the previous message in the specified folder, or the current folder if not specified
625     '''
626     folder, arglist = _argFolder(args, state['folder'])
627     state['folder'] = folder
628     try:
629         cur = int(state[folder+'.cur']) - 1
630     except KeyError:
631         cur = 1
632     state[folder+'.cur'] = _show(folder, str(cur))
633
634 def scan(args):
635     '''Usage: scan [+<folder>] [messageset]
636     Show a list of the specified messages (or all if unspecified)
637     in the specified folder, or the current folder if not specified
638     '''
639     subjlen = 47
640     if len(args) > 99:
641         raise UsageError()
642     # find any folder refs and put together the msgset string
643     folder, arglist = _argFolder(args, state['folder'])
644     state['folder'] = folder
645     msgset = _fixupMsgset(' '.join(arglist))
646     if not msgset:
647         msgset = "1:*"
648     _checkMsgset(msgset)
649     S = _connect()
650     do_or_die(S.select(folder), "Problem changing to folder:" )
651     try:
652         result, data = S.fetch(msgset, '(ENVELOPE FLAGS)')
653     except: pass
654     _debug('result: %s' % repr(result))
655     _debug('data: %s' % repr(data))
656     do_or_die([result, data], "Problem with fetch:" )
657     # take out fake/ba hits
658     data = [ hit for hit in data if hit and ' ' in hit ]
659     if data == [] or data[0] is None:
660         print "No messages."
661         sys.exit(0)
662     try:
663         cur = string.atoi(state[folder+'.cur'])
664     except:
665         cur = None
666     for hit in data:
667         _debug('Hit: %s' % (repr(hit)))
668         num, e = hit.split(' ',1)
669         num = string.atoi(num)
670         _debug("e: %s" % repr(e))
671         e = readsexpr(e)
672         env_date, env_subject, env_from, env_sender = e[1][:4]
673         flags = [str(f) for f in e[3]]
674         _debug("env_date: %s" % repr(env_date))
675         _debug("env_subject: %s" % repr(env_subject))
676         _debug("env_from: %s" % repr(env_from))
677         _debug("env_sender: %s" % repr(env_sender))
678         _debug("flags: %s" % repr(flags))
679         try:
680             dt = time.strptime(' '.join(str(env_date).split()[:5]), "%a, %d %b %Y %H:%M:%S ")
681             outtime = time.strftime("%m/%d", dt)
682         except:
683             outtime = "??/??"
684         if type(env_from) == type([]):
685             outfrom = str(env_from[0][0])
686             if outfrom == 'NIL':
687                 outfrom = "%s@%s" % (env_from[0][2], env_from[0][3])
688         else:
689             outfrom = "<Unknown>"
690         outsubj = str(env_subject)
691         if outsubj == 'NIL':
692             outsubj = "<no subject>"
693         if cur == num:
694             status = '>'
695         elif 'Answered' in flags:
696             status = 'r'
697         elif 'Seen' in flags:
698             status = ' '
699         elif 'Recent' in flags:
700             status = 'N'
701         else:
702             status = 'O'
703         outline = '%4s %s %s %-18s '% (num, status, outtime, outfrom[:18])
704         print outline + outsubj[:subjlen]
705     S.close()
706     S.logout()
707
708
709 def debug(args):
710     global Debug
711     Debug = 4   
712     _dispatch([sys.argv[0]]+args)
713
714 def help(args):
715     '''Usage: help <command>
716     Shows help on the specified command.
717     '''
718
719     def _sort(foo):
720         bar = foo
721         bar.sort()
722         return bar
723
724     if len(args) < 1:
725         print help.__doc__
726         print "Valid commands: %s " % (', '.join(_sort(Commands.keys())))
727         sys.exit(0)
728     else:
729         cmd = args[0]
730         cmdfunc = Commands.get(cmd, None)
731         print "Help on %s:\n" % cmd
732         print cmdfunc.__doc__
733
734
735 Commands = { 'folders': folders,
736              'folder': folder,
737              'debug': debug,
738              'pick': pick,
739              'refile': refile,
740              'rmf': rmf,
741              'rmm': rmm,
742              'scan': scan,
743              'search': pick,
744              'show': show,
745              'next': next,
746              'prev': prev,
747              'comp': comp,
748              'repl': repl,
749              'help': help,
750              'mr': mr
751            }
752
753
754 def _dispatch(args):
755
756     def _sort(foo):
757         bar = foo
758         bar.sort()
759         return bar
760
761     _debug("args: %s" % repr(args))
762     if len(args) > 1:
763         cmd = args[1]
764         _debug("cmd: %s" % cmd)
765         cmdargs = args[2:]
766         _debug("cmdargs: %s" % cmdargs)
767         _debug("commands: %s" % Commands)
768         cmdfunc = Commands.get(cmd,None)
769         if cmdfunc:
770             _debug("cmdfunc: %s" % cmdfunc)
771             try:
772                 cmdfunc(cmdargs)
773             except IOError: pass
774             except UsageError:
775                 print cmdfunc.__doc__
776                 sys.exit(1)
777             config.write()
778             state.write()
779         else:       
780             print "Unknown command %s.  Valid ones: %s " % (sys.argv[1], ', '.join(_sort(Commands.keys())))
781     else:
782         print "Must specify a command.  Valid ones: %s " % ', '.join(_sort(Commands.keys()))
783
784 # main program
785
786 if __name__ == '__main__':
787     if sys.stdout.isatty():
788        pager = os.environ.get('PAGER', None)
789        if pager is None:
790            for p in [ '/usr/bin/less', '/bin/more' ]:
791                if os.path.exists(p):
792                    pager = p
793                    break
794        if pager is not None:
795            sys.stdout = os.popen(pager, 'w')
796     try:
797         _dispatch(sys.argv)
798     except KeyboardInterrupt:
799         print "Interrupted."
800
Note: See TracBrowser for help on using the browser.