| 82 | |
| 83 | def to_unicode(self,text,charset): |
| 84 | default_charset = self.env.config.get('mailarchive', 'default_charset',None) |
| 85 | if default_charset : |
| 86 | chaerset = default_charset |
| 87 | |
| 88 | # to unicode with codecaliases |
| 89 | # codecaliases change mail charset to python charset |
| 90 | charset = charset.lower( ) |
| 91 | aliases = {} |
| 92 | aliases_text = self.env.config.get('mailarchive', 'codecaliases') |
| 93 | for alias in aliases_text.split(','): |
| 94 | alias_s = alias.split(':') |
| 95 | if len(alias_s) >=2: |
| 96 | if alias_s[1] == 'cmd': |
| 97 | aliases[alias_s[0].lower()] = ('cmd',alias_s[2]) |
| 98 | else: |
| 99 | aliases[alias_s[0].lower()] = ('codec',alias_s[1]) |
| 100 | |
| 101 | if aliases.has_key(charset): |
| 102 | (type,alias) = aliases[charset] |
| 103 | if type == 'codec': |
| 104 | text = unicode(text,alias) |
| 105 | elif type == 'cmd': |
| 106 | np = NaivePopen(alias, text, capturestderr=1) |
| 107 | if np.errorlevel or np.err: |
| 108 | err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, |
| 109 | np.err) |
| 110 | raise Exception, err |
| 111 | text = unicode(np.out,'utf-8') |
| 112 | else: |
| 113 | text = unicode(text,charset) |
| 114 | return text |
| 115 | |
| 116 | def import_message(self, msg, author,mlid, db): |
| 117 | OUTPUT_ENCODING = 'utf-8' |
| 118 | subject = '' |
| 119 | messageid = '' |
| 120 | utcdate = 0 |
| 121 | localdate = 0 |
| 122 | zoneoffset = 0 |
| 123 | text = '' |
| 124 | fromtext = '' |
| 125 | body = '' |
| 126 | ref_messageid = '' |
| 127 | |
| 128 | cursor = db.cursor() |
| 129 | is_newid = False |
| 130 | |
| 131 | if 'message-id' in msg: |
| 132 | messageid = msg['message-id'] |
| 133 | if messageid[:1] == '<': |
| 134 | messageid = messageid[1:] |
| 135 | if messageid[-1:] == '>': |
| 136 | messageid = messageid[:-1] |
| 137 | self.print_debug('Message-ID:%s' % messageid ) |
| 138 | |
| 139 | #check messageid is unique |
| 140 | self.print_debug("Creating new mailarc '%s'" % 'mailarc') |
| 141 | cursor.execute("SELECT id from mailarc WHERE messageid=%s",(messageid,)) |
| 142 | row = cursor.fetchone() |
| 143 | id = None |
| 144 | if row: |
| 145 | id = row[0] |
| 146 | if id == None or id == "": |
| 147 | # why? get_last_id return 0 at first. |
| 148 | #id = db.get_last_id(cursor, 'mailarc') |
| 149 | is_newid = True |
| 150 | cursor.execute("SELECT Max(id)+1 as id from mailarc") |
| 151 | row = cursor.fetchone() |
| 152 | if row and row[0] != None: |
| 153 | id = row[0] |
| 154 | else: |
| 155 | id = 1 |
| 156 | id = int(id) # Because id might be 'n.0', int() is called. |
| 157 | |
| 158 | |
| 159 | if 'date' in msg: |
| 160 | datetuple_tz = email.Utils.parsedate_tz(msg['date']) |
| 161 | localdate = calendar.timegm(datetuple_tz[:9]) #toDB |
| 162 | zoneoffset = datetuple_tz[9] # toDB |
| 163 | utcdate = localdate-zoneoffset # toDB |
| 164 | #make zone ( +HHMM or -HHMM |
| 165 | zone = '' |
| 166 | if zoneoffset >0: |
| 167 | zone = '+' + time.strftime('%H%M',time.gmtime(zoneoffset)) |
| 168 | elif zoneoffset < 0: |
| 169 | zone = '-' + time.strftime('%H%M',time.gmtime(-1*zoneoffset)) |
| 170 | |
| 171 | #self.print_debug( time.strftime("%y/%m/%d %H:%M:%S %z",datetuple_tz[:9])) |
| 172 | self.print_debug( time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(utcdate))) |
| 173 | self.print_debug( time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(localdate))) |
| 174 | self.print_debug(zone) |
| 175 | |
| 176 | fromname,fromaddr = email.Utils.parseaddr(msg['from']) |
| 177 | fromname = self.decode_to_unicode(fromname) |
| 178 | fromaddr = self.decode_to_unicode(fromaddr) |
| 179 | |
| 180 | self.print_info( ' ' + time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(localdate))+' ' + zone +' '+ fromaddr) |
| 181 | |
| 182 | if 'subject' in msg: |
| 183 | subject = self.decode_to_unicode(msg['subject']) |
| 184 | self.print_debug( subject.encode(OUTPUT_ENCODING)) |
| 185 | |
| 186 | # make thread infomations |
| 187 | ref_messageid = '' |
| 188 | if 'in-reply-to' in msg: |
| 189 | ref_messageid = ref_messageid + msg['In-Reply-To'] + ' ' |
| 190 | self.print_debug('In-Reply-To:%s' % ref_messageid ) |
| 191 | |
| 192 | if 'references' in msg: |
| 193 | ref_messageid = ref_messageid + msg['References'] + ' ' |
| 194 | |
| 195 | m = re.findall(r'<(.+?)>', ref_messageid) |
| 196 | ref_messageid = '' |
| 197 | for text in m: |
| 198 | ref_messageid = ref_messageid + "'%s'," % text |
| 199 | ref_messageid = ref_messageid.strip(',') |
| 200 | self.print_debug('RefMessage-ID:%s' % ref_messageid ) |
| 201 | |
| 202 | |
| 203 | # multipart mail |
| 204 | if msg.is_multipart(): |
| 205 | body = '' |
| 206 | # delete all attachement at message-id |
| 207 | Attachment.delete_all(self.env, 'mailarchive', id, db) |
| 208 | |
| 209 | for part in msg.walk(): |
| 210 | content_type = part.get_content_type() |
| 211 | self.print_debug('Content-Type:'+content_type) |
| 212 | file_counter = 1 |
| 213 | |
| 214 | if content_type == 'multipart/mixed': |
| 215 | pass |
| 216 | elif content_type == 'text/html' and self.is_file(part) == False: |
| 217 | body = part.get_payload(decode=1) |
| 218 | elif content_type == 'text/plain' and self.is_file(part) == False: |
| 219 | body = part.get_payload(decode=1) |
| 220 | charset = part.get_content_charset() |
| 221 | self.print_debug('charset:'+str(charset)) |
| 222 | # Todo:need try |
| 223 | if charset != None: |
| 224 | body = self.to_unicode(body,charset) |
| 225 | elif part.get_payload(decode=1) == None: |
| 226 | pass |
| 227 | else: |
| 228 | self.print_debug( part.get_content_type()) |
| 229 | # get filename |
| 230 | # Applications should really sanitize the given filename so that an |
| 231 | # email message can't be used to overwrite important files |
| 232 | filename = self.get_filename(part) |
| 233 | if not filename: |
| 234 | ext = mimetypes.guess_extension(part.get_content_type()) |
| 235 | if not ext: |
| 236 | # Use a generic bag-of-bits extension |
| 237 | ext = '.bin' |
| 238 | filename = 'part-%03d%s' % (file_counter, ext) |
| 239 | file_counter += 1 |
| 240 | |
| 241 | self.print_debug("filename:" + filename.encode(OUTPUT_ENCODING)) |
| 242 | |
| 243 | # make attachment |
| 244 | tmp = os.tmpfile() |
| 245 | tempsize =len(part.get_payload(decode=1)) |
| 246 | tmp.write(part.get_payload(decode=1)) |
| 247 | |
| 248 | tmp.flush() |
| 249 | tmp.seek(0,0) |
| 250 | |
| 251 | attachment = Attachment(self.env,'mailarchive', id) |
| 252 | |
| 253 | attachment.description = '' # req.args.get('description', '') |
| 254 | attachment.author = author #req.args.get('author', '') |
| 255 | attachment.ipnr = '127.0.0.1' |
| 256 | |
| 257 | try: |
| 258 | attachment.insert(filename, |
| 259 | tmp, tempsize,None,db) |
| 260 | except Exception, e: |
| 261 | try: |
| 262 | ext = filename.split('.')[-1] |
| 263 | if ext == filename: |
| 264 | ext = '.bin' |
| 265 | else: |
| 266 | ext = '.' + ext |
| 267 | filename = 'part-%03d%s' % (file_counter, ext) |
| 268 | file_counter += 1 |
| 269 | attachment.insert(filename, |
| 270 | tmp, tempsize,None,db) |
| 271 | self.print_warning('As name is too long, the attached file is renamed : '+filename) |
| 272 | |
| 273 | except Exception, e: |
| 274 | self.print_error('Exception at attach file of Message-ID:'+messageid) |
| 275 | self.print_error( e ) |
| 276 | |
| 277 | tmp.close() |
| 278 | |
| 279 | # not multipart mail |
| 280 | else: |
| 281 | # Todo:if Content-Type = text/html then convert htmlMail to text |
| 282 | content_type = msg.get_content_type() |
| 283 | self.print_debug('Content-Type:'+content_type) |
| 284 | if content_type == 'text/html': |
| 285 | body = 'html' |
| 286 | else: |
| 287 | #body |
| 288 | #self.print_debug(msg.get_content_type()) |
| 289 | body = msg.get_payload(decode=1) |
| 290 | charset = msg.get_content_charset() |
| 291 | |
| 292 | # need try: |
| 293 | if charset != None: |
| 294 | self.print_debug("charset:"+charset) |
| 295 | body = self.to_unicode(body,charset) |
| 296 | |
| 297 | |
| 298 | #body = body.replace(os.linesep,'\n') |
| 299 | self.print_debug('Thread') |
| 300 | |
| 301 | thread_parent = ref_messageid.replace("'",'').replace(',',' ') |
| 302 | thread_root = '' |
| 303 | if thread_parent !='': |
| 304 | # sarch first parent id |
| 305 | self.print_debug("SearchThread;"+thread_parent) |
| 306 | cursor = db.cursor() |
| 307 | sql = "SELECT threadroot,messageid FROM mailarc where messageid in (%s)" % ref_messageid |
| 308 | self.print_debug(sql) |
| 309 | cursor.execute(sql) |
| 310 | |
| 311 | row = cursor.fetchone() |
| 312 | if row: |
| 313 | #thread_parent = row[1] |
| 314 | if row[0] == '': |
| 315 | thread_root = thread_parent.split(' ').pop() |
| 316 | self.print_debug("AddToThread;"+thread_root) |
| 317 | else: |
| 318 | thread_root = row[0] |
| 319 | self.print_debug("NewThread;"+thread_root) |
| 320 | else: |
| 321 | self.print_debug("NoThread;"+thread_parent) |
| 322 | thread_root = thread_root.strip() |
| 323 | |
| 324 | self.print_debug('Insert') |
| 325 | |
| 326 | if messageid != '': |
| 327 | |
| 328 | # insert or update mailarc_category |
| 329 | |
| 330 | yearmonth = time.strftime("%Y%m",time.gmtime(utcdate)) |
| 331 | category = mlid+yearmonth |
| 332 | cursor.execute("SELECT category,mlid,yearmonth,count FROM mailarc_category WHERE category=%s",(category.encode('utf-8'),)) |
| 333 | row = cursor.fetchone() |
| 334 | count = 0 |
| 335 | if row: |
| 336 | count = row[3] |
| 337 | pass |
| 338 | else: |
| 339 | cursor.execute("INSERT INTO mailarc_category (category,mlid,yearmonth,count) VALUES(%s,%s,%s,%s)",(category.encode('utf-8'),mlid.encode('utf-8'),yearmonth,0)) |
| 340 | if is_newid == True: |
| 341 | count = count +1 |
| 342 | cursor.execute("UPDATE mailarc_category SET count=%s WHERE category=%s" , |
| 343 | (count,category.encode('utf-8'))) |
| 344 | |
| 345 | # insert or update mailarc |
| 346 | |
| 347 | #self.print_debug( |
| 348 | # "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)" %(str(id), |
| 349 | # category.encode('utf-8'), |
| 350 | # messageid, |
| 351 | # utcdate, |
| 352 | # zoneoffset, |
| 353 | # subject.encode('utf-8'), fromname.encode('utf-8'), |
| 354 | # fromaddr.encode('utf-8'),'','', |
| 355 | # thread_root,thread_parent)) |
| 356 | cursor.execute("DELETE FROM mailarc where messageid=%s",(messageid,)) |
| 357 | cursor.execute("INSERT INTO mailarc (" |
| 358 | "id,category,messageid,utcdate,zoneoffset,subject," |
| 359 | "fromname,fromaddr,header,text, threadroot,threadparent ) " |
| 360 | "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", |
| 361 | (str(id), |
| 362 | category.encode('utf-8'), |
| 363 | messageid, |
| 364 | utcdate, |
| 365 | zoneoffset, |
| 366 | subject.encode('utf-8'), fromname.encode('utf-8'), |
| 367 | fromaddr.encode('utf-8'),'',body.encode('utf-8'), |
| 368 | thread_root,thread_parent)) |
| 369 | |
| 370 | db.commit() |
| 371 | |
| 372 | def do_refresh_category(self,line): |
| 373 | db = self.db_open() |
| 374 | self.env = self.env_open() |
| 375 | cursor = db.cursor() |
| 376 | cursor.execute("DELETE FROM mailarc_category") |
| 377 | cursor.execute("SELECT category, count(*) as cnt from mailarc GROUP BY category ") |
| 378 | for category,cnt in cursor: |
| 379 | cursor2 = db.cursor() |
| 380 | cursor2.execute("INSERT INTO mailarc_category (category,mlid,yearmonth,count) VALUES(%s,%s,%s,%s)",(category,category[:-6],category[-6:],cnt)) |
| 381 | db.commit() |
| 382 | |
| 383 | ## Help |
| 384 | _help_import = [('import <mlname> <filepath>', 'import UnixMail')] |
| 385 | |
| 386 | def do_import(self,line): |
| 387 | arg = self.arg_tokenize(line) |
| 388 | if len(arg) < 2 : |
| 389 | print "import MLname filepath" |
| 390 | db = self.db_open() |
| 391 | self.env = self.env_open() |
| 392 | self._import_unixmailbox('cmd',db,arg[0],arg[1]) |
| 393 | |
| 394 | ## Help |
| 395 | _help_pop3 = [('pop3 <mlname>', 'import from pop3 server')] |
| 396 | |
| 397 | def do_pop3(self,line): |
| 398 | arg = self.arg_tokenize(line) |
| 399 | if len(arg) < 1 : |
| 400 | print "pop3 MLname" |
| 401 | db = self.db_open() |
| 402 | self.env = self.env_open() |
| 403 | self._import_from_pop3('cmd',db,arg[0]) |
| 404 | |
| 405 | ## Help |
| 406 | _help_help = [('help', 'Show documentation')] |
| 407 | |
| 408 | def do_help(self, line=None): |
| 409 | arg = self.arg_tokenize(line) |
| 410 | if arg[0]: |
| 411 | try: |
| 412 | doc = getattr(self, "_help_" + arg[0]) |
| 413 | self.print_doc (doc) |
| 414 | except AttributeError: |
| 415 | print "No documentation found for '%s'" % arg[0] |
| 416 | else: |
| 417 | docs = (#self._help_about + |
| 418 | self._help_help + |
| 419 | self._help_import + self._help_pop3 |
| 420 | ) |
| 421 | print 'mailarc-admin - The Trac MailArchivePlugin Administration Console ' |
| 422 | if not self.interactive: |
| 423 | print |
| 424 | print "Usage: mailarc-admin </path/to/projenv> [command [subcommand] [option ...]]\n" |
| 425 | print "Invoking mailarc-admin without command starts "\ |
| 426 | "interactive mode." |
| 427 | self.print_doc (docs) |
| 428 | |
| 429 | |
| 430 | |
| 431 | def print_info(self,line): |
| 432 | print "%s" % line |
| 433 | |
| 434 | def print_debug(self,line): |
| 435 | #print "[Debug] %s" % line |
| 436 | pass |
| 437 | |
| 438 | def print_error(self,line): |
| 439 | print "[Error] %s" % line |
| 440 | |
| 441 | def print_warning(self,line): |
| 442 | print "[Warning] %s" % line |
| 443 | |
| 444 | def _import_unixmailbox(self,author, db, mlid, msgfile_path): |
| 445 | self.print_debug('import_mail') |
| 446 | if not db: |
| 447 | #db = self.env.get_db_cnx() |
| 448 | handle_ta = True |
| 449 | else: |
| 450 | handle_ta = False |
| 451 | |
| 452 | |
| 453 | #paser = Parser() |
| 454 | |
| 455 | self.print_info("%s Start Importing %s ..." % |
| 456 | (time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime()),msgfile_path)) |
| 457 | |
| 458 | fp = open(msgfile_path,"rb") |
| 459 | mbox = mailbox.UnixMailbox(fp, self.msgfactory) |
| 460 | |
| 461 | counter =1 |
| 462 | msg = mbox.next() |
| 463 | while msg is not None: |
| 464 | messageid = '' |
| 465 | try: |
| 466 | messageid = msg['message-id'] |
| 467 | self.import_message(msg,author,mlid,db) |
| 468 | except Exception, e: |
| 469 | exception_flag = True |
| 470 | self.print_error('Exception At Message-ID:'+messageid) |
| 471 | self.print_error( e ) |
| 472 | #traceback.print_exc() |
| 473 | |
| 474 | if counter > 10000: |
| 475 | break |
| 476 | msg = mbox.next() |
| 477 | counter = counter + 1 |
| 478 | |
| 479 | |
| 480 | fp.close() |
| 481 | #if handle_ta: |
| 482 | db.commit() |
| 483 | self.print_info("End Imporing %s. " % msgfile_path) |
| 484 | |
| 485 | def _import_from_pop3(self,author, db, mlid): |
| 486 | |
| 487 | pop_server = self.env.config.get('mailarchive', 'pop3_server') |
| 488 | pop_user = self.env.config.get('mailarchive', 'pop3_user') |
| 489 | pop_password = self.env.config.get('mailarchive', 'pop3_password') |
| 490 | pop_delete = self.env.config.get('mailarchive', 'pop3_delete','none') |
| 491 | |
| 492 | if pop_server =='': |
| 493 | self.print_error('trac.ini mailarchive pop3_server is null!') |
| 494 | elif pop_user == '': |
| 495 | self.print_error('trac.ini mailarchive pop3_user is null!') |
| 496 | elif pop_password == '': |
| 497 | self.print_error('trac.ini mailarchive pop3_password is null!') |
| 498 | |
| 499 | self.print_info("%s Start Connction pop3 %s:%s ..." % |
| 500 | (time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime()), |
| 501 | pop_server,pop_user)) |
| 502 | |
| 503 | pop = poplib.POP3(pop_server) |
| 504 | pop.user(pop_user) |
| 505 | pop.pass_(pop_password) |
| 506 | num_messages = len(pop.list()[1]) |
| 507 | counter = 1 |
| 508 | for i in range(num_messages): |
| 509 | #lines = ['',] |
| 510 | #for j in pop.retr(i+1)[1]: |
| 511 | # lines.append(j + os.linesep) |
| 512 | #mes_text = ''.join(lines) |
| 513 | mes_text = ''.join(['%s\n' % line for line in pop.retr(i+1)[1]]) |
| 514 | messageid = '' |
| 515 | exception_flag = False |
| 516 | try: |
| 517 | msg = email.message_from_string(mes_text) |
| 518 | messageid = msg['message-id'] |
| 519 | self.import_message(msg,author,mlid,db) |
| 520 | except Exception, e: |
| 521 | exception_flag = True |
| 522 | self.print_error('Exception At Message-ID:'+messageid) |
| 523 | self.print_error( e ) |
| 524 | |
| 525 | #if exception_flag == False: |
| 526 | # self.print_info(" Import Message Success") |
| 527 | |
| 528 | |
| 529 | # delete mail |
| 530 | if pop_delete == 'all': |
| 531 | pop.dele(i+1) |
| 532 | self.print_info(" Delete MailServer Message ") |
| 533 | elif pop_delete == 'imported': |
| 534 | if exception_flag == False: |
| 535 | pop.dele(i+1) |
| 536 | self.print_info(" Delete MailServer Message ") |
| 537 | else: |
| 538 | pass |
| 539 | |
| 540 | if counter > 10000: |
| 541 | break |
| 542 | counter = counter + 1 |
| 543 | |
| 544 | pop.quit() |
| 545 | |
| 546 | #if handle_ta: |
| 547 | db.commit() |
| 548 | self.print_info("End Reciving. " ) |
| 549 | |
| 550 | def is_file(self,part ): |
| 551 | """Return True:filename associated with the payload if present. |
| 552 | """ |
| 553 | missing = object() |
| 554 | filename = part.get_param('filename', missing, 'content-disposition') |
| 555 | if filename is missing: |
| 556 | filename = part.get_param('name', missing, 'content-disposition') |
| 557 | if filename is missing: |
| 558 | return False |
| 559 | return True |
| 560 | |
| 561 | def get_filename(self,part , failobj=None): |
| 562 | """Return the filename associated with the payload if present. |
| 563 | |
| 564 | The filename is extracted from the Content-Disposition header's |
| 565 | `filename' parameter, and it is unquoted. If that header is missing |
| 566 | the `filename' parameter, this method falls back to looking for the |
| 567 | `name' parameter. |
| 568 | """ |
| 569 | missing = object() |
| 570 | filename = part.get_param('filename', missing, 'content-disposition') |
| 571 | if filename is missing: |
| 572 | filename = part.get_param('name', missing, 'content-disposition') |
| 573 | if filename is missing: |
| 574 | return failobj |
| 575 | |
| 576 | errors='replace' |
| 577 | fallback_charset='us-ascii' |
| 578 | if isinstance(filename, tuple): |
| 579 | rawval = self.unquote(filename[2]) |
| 580 | charset = filename[0] or 'us-ascii' |
| 581 | try: |
| 582 | return self.to_unicode(rawval, charset) |
| 583 | except LookupError: |
| 584 | # XXX charset is unknown to Python. |
| 585 | return unicode(rawval, fallback_charset, errors) |
| 586 | else: |
| 587 | return self.decode_to_unicode(self.unquote(value)) |
| 588 | |
| 589 | def unquote(self,str): |
| 590 | """Remove quotes from a string.""" |
| 591 | if len(str) > 1: |
| 592 | if str.startswith('"') and str.endswith('"'): |
| 593 | return str[1:-1].replace('\\\\', '\\').replace('\\"', '"') |
| 594 | if str.startswith('<') and str.endswith('>'): |
| 595 | return str[1:-1] |
| 596 | return str |
| 597 | |
| 598 | def _delete_message(self,db,id): |
| 599 | pass |
| 600 | |
| 601 | def run(args): |
| 602 | """Main entry point.""" |
| 603 | admin = MailArchiveAdmin() |
| 604 | if len(args) > 0: |
| 605 | if args[0] in ('-h', '--help', 'help'): |
| 606 | return admin.onecmd("help") |
| 607 | elif args[0] in ('-v','--version','about'): |
| 608 | return admin.onecmd("about") |
| 609 | else: |
| 610 | admin.env_set(os.path.abspath(args[0])) |
| 611 | if len(args) > 1: |
| 612 | s_args = ' '.join(["'%s'" % c for c in args[2:]]) |
| 613 | command = args[1] + ' ' +s_args |
| 614 | return admin.onecmd(command) |
| 615 | else: |
| 616 | while True: |
| 617 | admin.run() |
| 618 | else: |
| 619 | return admin.onecmd("help") |
| 620 | |
| 621 | sys.exit(run(sys.argv[1:])) |