| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 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 |
|
|---|
| 245 |
cur = state.get(state['folder']+'.cur', None) |
|---|
| 246 |
if cur == 'None': cur = None |
|---|
| 247 |
if cur is not None: |
|---|
| 248 |
|
|---|
| 249 |
|
|---|
| 250 |
msgset = msgset.replace('cur', cur) |
|---|
| 251 |
|
|---|
| 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 |
|
|---|
| 270 |
|
|---|
| 271 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 331 |
errcount = _SMTPsend(tmpfile) |
|---|
| 332 |
if not errcount: |
|---|
| 333 |
os.unlink(tmpfile) |
|---|
| 334 |
else: |
|---|
| 335 |
|
|---|
| 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 |
|
|---|
| 347 |
ret = _edit(tmpfile) |
|---|
| 348 |
if ret == 0: |
|---|
| 349 |
|
|---|
| 350 |
_SMTPsend(tmpfile) |
|---|
| 351 |
if not errcount: |
|---|
| 352 |
os.unlink(tmpfile) |
|---|
| 353 |
else: |
|---|
| 354 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|