Changeset 507 for trunk/email2trac.py.in
- Timestamp:
- 01/11/11 14:30:31 (13 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/email2trac.py.in
r506 r507 50 50 * Commandline opions: 51 51 -h,--help 52 52 -d, --debug 53 53 -f,--file <configuration file> 54 54 -n,--dry-run … … 89 89 90 90 class SaraDict(UserDict.UserDict): 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 91 def __init__(self, dictin = None): 92 UserDict.UserDict.__init__(self) 93 self.name = None 94 95 if dictin: 96 if dictin.has_key('name'): 97 self.name = dictin['name'] 98 del dictin['name'] 99 self.data = dictin 100 101 def get_value(self, name): 102 if self.has_key(name): 103 return self[name] 104 else: 105 return None 106 107 def __repr__(self): 108 return repr(self.data) 109 110 def __str__(self): 111 return str(self.data) 112 113 def __getattr__(self, name): 114 """ 115 override the class attribute get method. Return the value 116 from the dictionary 117 """ 118 if self.data.has_key(name): 119 return self.data[name] 120 else: 121 return None 122 123 def __setattr__(self, name, value): 124 """ 125 override the class attribute set method only when the UserDict 126 has set its class attribute 127 """ 128 if self.__dict__.has_key('data'): 129 self.data[name] = value 130 else: 131 self.__dict__[name] = value 132 133 def __iter__(self): 134 return iter(self.data.keys()) 135 135 136 136 class TicketEmailParser(object): 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 137 env = None 138 comment = '> ' 139 140 def __init__(self, env, parameters, logger, version): 141 self.env = env 142 143 # Database connection 144 # 145 self.db = None 146 147 # Save parameters 148 # 149 self.parameters = parameters 150 self.logger = logger 151 152 # Some useful mail constants 153 # 154 self.email_name = None 155 self.email_addr = None 156 self.email_from = None 157 self.author = None 158 self.id = None 159 160 self.STRIP_CONTENT_TYPES = list() 161 162 ## fields properties via body_text 163 # 164 self.properties = dict() 165 166 self.VERSION = version 167 168 self.get_config = self.env.config.get 169 170 ## init function ## 171 # 172 self.setup_parameters() 173 174 def setup_parameters(self): 175 if self.parameters.umask: 176 os.umask(self.parameters.umask) 177 178 if not self.parameters.spam_level: 179 self.parameters.spam_level = 0 180 181 if not self.parameters.spam_header: 182 self.parameters.spam_header = 'X-Spam-Score' 183 184 if not self.parameters.email_quote: 185 self.parameters.email_quote = '> ' 186 187 if not self.parameters.ticket_update_by_subject_lookback: 188 self.parameters.ticket_update_by_subject_lookback = 30 189 190 if self.parameters.verbatim_format == None: 191 self.parameters.verbatim_format = 1 192 193 if self.parameters.reflow == None: 194 self.parameters.reflow = 1 195 196 if self.parameters.binhex: 197 self.STRIP_CONTENT_TYPES.append('application/mac-binhex40') 198 199 if self.parameters.applesingle: 200 self.STRIP_CONTENT_TYPES.append('application/applefile') 201 202 if self.parameters.appledouble: 203 self.STRIP_CONTENT_TYPES.append('application/applefile') 204 205 if self.parameters.strip_content_types: 206 items = self.parameters.strip_content_types.split(',') 207 for item in items: 208 self.STRIP_CONTENT_TYPES.append(item.strip()) 209 210 if self.parameters.tmpdir: 211 self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir'])) 212 else: 213 self.parameters.tmpdir = os.path.normcase('/tmp') 214 215 if self.parameters.email_triggers_workflow == None: 216 self.parameters.email_triggers_workflow = 1 217 218 if not self.parameters.subject_field_separator: 219 self.parameters.subject_field_separator = '&' 220 else: 221 self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip() 222 223 self.trac_smtp_from = self.get_config('notification', 'smtp_from') 224 self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain') 225 226 227 self.system = None 228 228 229 229 ########## Email Header Functions ########################################################### 230 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 elif message.has_key('X-Virus-found'): 251 252 253 254 255 256 257 258 return 'drop' 259 260 261 262 return 'Spam' 263 264 265 266 267 268 269 270 271 """ 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 #to_list.append(e)381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 231 def spam(self, message): 232 """ 233 # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_ 234 # Note if Spam_level then '*' are included 235 """ 236 spam = False 237 if message.has_key(self.parameters.spam_header): 238 spam_l = string.split(message[self.parameters.spam_header]) 239 240 try: 241 number = spam_l[0].count('*') 242 except IndexError, detail: 243 number = 0 244 245 if number >= self.parameters.spam_level: 246 spam = True 247 248 # treat virus mails as spam 249 # 250 elif message.has_key('X-Virus-found'): 251 spam = True 252 253 # How to handle SPAM messages 254 # 255 if self.parameters.drop_spam and spam: 256 257 self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level) 258 return 'drop' 259 260 elif spam: 261 262 return 'Spam' 263 else: 264 265 return False 266 267 def email_header_acl(self, keyword, header_field, default): 268 """ 269 This function wil check if the email address is allowed or denied 270 to send mail to the ticket list 271 """ 272 self.logger.debug('function email_header_acl: %s' %keyword) 273 274 try: 275 mail_addresses = self.parameters[keyword] 276 277 # Check if we have an empty string 278 # 279 if not mail_addresses: 280 return default 281 282 except KeyError, detail: 283 self.logger.debug('%s not defined, all messages are allowed.' %(keyword)) 284 285 return default 286 287 mail_addresses = string.split(mail_addresses, ',') 288 289 for entry in mail_addresses: 290 entry = entry.strip() 291 TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE) 292 result = TO_RE.search(header_field) 293 if result: 294 return True 295 296 return False 297 298 def email_header_txt(self, m): 299 """ 300 Display To and CC addresses in description field 301 """ 302 s = '' 303 304 if m['To'] and len(m['To']) > 0: 305 s = "'''To:''' %s\r\n" %(m['To']) 306 if m['Cc'] and len(m['Cc']) > 0: 307 s = "%s'''Cc:''' %s\r\n" % (s, m['Cc']) 308 309 return self.email_to_unicode(s) 310 311 312 def get_sender_info(self, message): 313 """ 314 Get the default author name and email address from the message 315 """ 316 317 self.email_to = self.email_to_unicode(message['to']) 318 self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to) 319 320 self.email_from = self.email_to_unicode(message['from']) 321 self.email_name, self.email_addr = email.Utils.parseaddr(self.email_from) 322 323 ## Trac can not handle author's name that contains spaces 324 # 325 if self.email_addr == self.trac_smtp_from: 326 if self.email_name: 327 self.author = self.email_name 328 else: 329 self.author = "email2trac" 330 else: 331 self.author = self.email_addr 332 333 if self.parameters.ignore_trac_user_settings: 334 return 335 336 # Is this a registered user, use email address as search key: 337 # result: 338 # u : login name 339 # n : Name that the user has set in the settings tab 340 # e : email address that the user has set in the settings tab 341 # 342 users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db) 343 if ( 344 (e and (e.lower() == self.email_addr.lower())) or 345 (u + '@' + self.smtp_default_domain.lower() == self.email_addr.lower()) 346 ) 347 ] 348 349 if len(users) >= 1: 350 self.email_from = users[0][0] 351 self.author = users[0][0] 352 353 def set_reply_fields(self, ticket, message): 354 """ 355 Set all the right fields for a new ticket 356 """ 357 self.logger.debug('function set_reply_fields') 358 359 ## Only use name or email adress 360 #ticket['reporter'] = self.email_from 361 ticket['reporter'] = self.author 362 363 364 # Put all CC-addresses in ticket CC field 365 # 366 if self.parameters.reply_all: 367 368 email_cc = '' 369 370 cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) ) 371 372 if not cc_addrs: 373 return 374 375 ## Build a list of forbidden CC addresses 376 # 377 #to_addrs = email.Utils.getaddresses( message.get_all('to', []) ) 378 #to_list = list() 379 #for n,e in to_addrs: 380 # to_list.append(e) 381 382 # Always Remove reporter email address from cc-list 383 # 384 try: 385 cc_addrs.remove((self.author, self.email_addr)) 386 except ValueError, detail: 387 pass 388 389 for name,addr in cc_addrs: 390 391 ## Prevent mail loop 392 # 393 #if addr in to_list: 394 395 if addr == self.trac_smtp_from: 396 self.logger.debug("Skipping %s mail address for CC-field" %(addr)) 397 continue 398 399 if email_cc: 400 email_cc = '%s, %s' %(email_cc, addr) 401 else: 402 email_cc = addr 403 404 if email_cc: 405 self.logger.debug('set_reply_fields: %s' %email_cc) 406 407 ticket['cc'] = self.email_to_unicode(email_cc) 408 408 409 409 410 410 ########## DEBUG functions ########################################################### 411 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 412 def debug_body(self, message_body, temporary_file=False): 413 if temporary_file: 414 body_file = tempfile.mktemp('.email2trac') 415 else: 416 body_file = os.path.join(self.parameters.tmpdir, 'body.txt') 417 418 if self.parameters.dry_run: 419 print 'DRY-RUN: not saving body to %s' %(body_file) 420 return 421 422 print 'writing body to %s' %(body_file) 423 fx = open(body_file, 'wb') 424 if not message_body: 425 message_body = '(None)' 426 427 message_body = message_body.encode('utf-8') 428 #message_body = unicode(message_body, 'iso-8859-15') 429 430 fx.write(message_body) 431 fx.close() 432 try: 433 os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO) 434 except OSError: 435 pass 436 437 def debug_attachments(self, message_parts): 438 """ 439 """ 440 self.logger.debug('function debug_attachments') 441 442 n = 0 443 for item in message_parts: 444 # Skip inline text parts 445 if not isinstance(item, tuple): 446 continue 447 448 (original, filename, part) = item 449 450 n = n + 1 451 print 'part%d: Content-Type: %s' % (n, part.get_content_type()) 452 453 s = 'part%d: filename: %s' %(n, filename) 454 self.print_unicode(s) 455 456 ## Forbidden chars 457 # 458 filename = filename.replace('\\', '_') 459 filename = filename.replace('/', '_') 460 461 462 part_file = os.path.join(self.parameters.tmpdir, filename) 463 s = 'writing part%d (%s)' % (n,part_file) 464 self.print_unicode(s) 465 466 if self.parameters.dry_run: 467 print 'DRY_RUN: NOT saving attachments' 468 continue 469 470 part_file = util.text.unicode_quote(part_file) 471 472 fx = open(part_file, 'wb') 473 text = part.get_payload(decode=1) 474 475 if not text: 476 text = '(None)' 477 478 fx.write(text) 479 fx.close() 480 481 try: 482 os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO) 483 except OSError: 484 pass 485 486 def save_email_for_debug(self, message, create_tempfile=False): 487 488 if create_tempfile: 489 msg_file = tempfile.mktemp('.email2trac') 490 else: 491 #msg_file = '/var/tmp/msg.txt' 492 msg_file = os.path.join(self.parameters.tmpdir, 'msg.txt') 493 494 if self.parameters.dry_run: 495 print 'DRY_RUN: NOT saving email message to %s' %(msg_file) 496 else: 497 print 'saving email to %s' %(msg_file) 498 499 fx = open(msg_file, 'wb') 500 fx.write('%s' % message) 501 fx.close() 502 503 try: 504 os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO) 505 except OSError: 506 pass 507 508 message_parts = self.get_message_parts(message) 509 message_parts = self.unique_attachment_names(message_parts) 510 body_text = self.get_body_text(message_parts) 511 self.debug_body(body_text, True) 512 self.debug_attachments(message_parts) 513 513 514 514 ########## Conversion functions ########################################################### 515 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 # 598 # 599 # 600 # 516 def email_to_unicode(self, message_str): 517 """ 518 Email has 7 bit ASCII code, convert it to unicode with the charset 519 that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac 520 understands it. 521 """ 522 self.logger.debug("function email_to_unicode") 523 524 results = email.Header.decode_header(message_str) 525 526 s = None 527 for text,format in results: 528 if format: 529 try: 530 temp = unicode(text, format) 531 except UnicodeError, detail: 532 # This always works 533 # 534 temp = unicode(text, 'iso-8859-15') 535 except LookupError, detail: 536 #text = 'ERROR: Could not find charset: %s, please install' %format 537 #temp = unicode(text, 'iso-8859-15') 538 temp = message_str 539 540 else: 541 temp = string.strip(text) 542 temp = unicode(text, 'iso-8859-15') 543 544 if s: 545 s = '%s %s' %(s, temp) 546 else: 547 s = '%s' %temp 548 549 #s = s.encode('utf-8') 550 return s 551 552 def str_to_dict(self, s): 553 """ 554 Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value> 555 """ 556 self.logger.debug("function str_to_dict") 557 558 fields = string.split(s, self.parameters.subject_field_separator) 559 560 result = dict() 561 for field in fields: 562 try: 563 index, value = string.split(field, '=') 564 565 # We can not change the description of a ticket via the subject 566 # line. The description is the body of the email 567 # 568 if index.lower() in ['description']: 569 continue 570 571 if value: 572 result[index.lower()] = value 573 574 except ValueError: 575 pass 576 return result 577 578 def print_unicode(self,s): 579 """ 580 This function prints unicode strings if possible else it will quote it 581 """ 582 try: 583 self.logger.debug(s) 584 except UnicodeEncodeError, detail: 585 self.logger.debug(util.text.unicode_quote(s)) 586 587 588 def html_2_txt(self, data): 589 """ 590 Various routines to convert html syntax to valid trac wiki syntax 591 """ 592 self.logger.debug('function html_2_txt') 593 594 ## This routine make an safe html that can be include 595 # in trac, but no further text processing can be done 596 # 597 # try: 598 # from lxml.html.clean import Cleaner 599 # tags_rm = list() 600 # tags_rm.append('body') 601 601 # 602 # 603 # 604 # 602 # cleaner = Cleaner(remove_tags=tags_rm ) 603 # parsed_data = cleaner.clean_html(data) 604 # parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n' 605 605 # 606 # 607 # 608 # 609 # 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 606 # return parsed_data 607 # 608 # except ImportError:: 609 # pass 610 611 parsed_data = None 612 if self.parameters.html2text_cmd: 613 tmp_file = tempfile.mktemp('email2trac.html') 614 cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file) 615 self.logger.debug('\t html2text conversion %s'%(cmd)) 616 617 if self.parameters.dry_run: 618 print 'DRY_RUN: html2text conversion command: %s\n' %(cmd) 619 620 else: 621 f = open(tmp_file, "w+") 622 f.write(data) 623 f.close() 624 625 lines = os.popen(cmd).readlines() 626 parsed_data = ''.join(lines) 627 628 os.unlink(tmp_file) 629 630 else: 631 self.logger.debug('\t No html2text conversion tool specified in email2trac.conf') 632 633 return parsed_data 634 634 635 635 ########## TRAC ticket functions ########################################################### 636 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 637 def check_permission_participants(self, tkt, action): 638 """ 639 Check if the mailer is allowed to update the ticket 640 """ 641 self.logger.debug('function check_permission_participants') 642 643 if tkt['reporter'].lower() in [self.author, self.email_addr]: 644 self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr)) 645 646 return True 647 648 perm = PermissionSystem(self.env) 649 if perm.check_permission(action, self.author): 650 self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author)) 651 652 return True 653 654 # Is the updater in the CC? 655 try: 656 cc_list = tkt['cc'].split(',') 657 for cc in cc_list: 658 if self.email_addr.lower() in cc.strip(): 659 self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr)) 660 661 return True 662 663 except KeyError: 664 pass 665 666 return False 667 668 def check_permission(self, tkt, action): 669 """ 670 check if the reporter has the right permission for the action: 671 671 - TICKET_CREATE 672 672 - TICKET_MODIFY … … 674 674 - TICKET_CHGPROP 675 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 return (self.check_permission_participants(tkt, action)) 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 #ticket[field] = value772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 else: 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 ## some routines in trac are dependend on ticket id 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 ## Save the attachments to the ticket 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 676 There are three models: 677 - None : no checking at all 678 - trac : check the permission via trac permission model 679 - email2trac: .... 680 """ 681 self.logger.debug("function check_permission") 682 683 if self.parameters.ticket_permission_system in ['trac']: 684 685 perm = PermissionSystem(self.env) 686 if perm.check_permission(action, self.author): 687 return True 688 else: 689 return False 690 691 elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']: 692 return (self.check_permission_participants(tkt, action)) 693 694 ## Default is to allow everybody ticket updates and ticket creation 695 # 696 else: 697 return True 698 699 700 def update_ticket_fields(self, ticket, user_dict, new=None): 701 """ 702 This will update the ticket fields. It will check if the 703 given fields are known and if the right values are specified 704 It will only update the ticket field value: 705 - If the field is known 706 - If the value supplied is valid for the ticket field. 707 If not then there are two options: 708 1) Skip the value (new=None) 709 2) Set default value for field (new=1) 710 """ 711 self.logger.debug("function update_ticket_fields") 712 713 ## Check only permission model on ticket updates 714 # 715 if not new: 716 if self.parameters.ticket_permission_system: 717 if not self.check_permission(ticket, 'TICKET_CHGPROP'): 718 self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author) 719 return False 720 721 ## Build a system dictionary from the ticket fields 722 # with field as index and option as value 723 # 724 sys_dict = dict() 725 for field in ticket.fields: 726 try: 727 sys_dict[field['name']] = field['options'] 728 729 except KeyError: 730 #sys_dict[field['name']] = None 731 pass 732 733 ## Check user supplied fields an compare them with the 734 # system one's 735 # 736 for field,value in user_dict.items(): 737 if self.parameters.debug: 738 s = 'user_field\t %s = %s' %(field,value) 739 self.print_unicode(s) 740 741 ## To prevent mail loop 742 # 743 if field == 'cc': 744 745 cc_list = user_dict['cc'].split(',') 746 747 if self.trac_smtp_from in cc_list: 748 self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)) 749 750 cc_list.remove(self.trac_smtp_from) 751 752 value = ','.join(cc_list) 753 754 755 ## Check if every value is allowed for this field 756 # 757 if sys_dict.has_key(field): 758 759 if value in sys_dict[field]: 760 ticket[field] = value 761 else: 762 ## Must we set a default if value is not allowed 763 # 764 if new: 765 value = self.get_config('ticket', 'default_%s' %(field) ) 766 767 else: 768 ## Only set if we have a value 769 # 770 #if value: 771 # ticket[field] = value 772 ticket[field] = value 773 774 if self.parameters.debug: 775 s = 'ticket_field\t %s = %s' %(field, ticket[field]) 776 self.print_unicode(s) 777 778 def ticket_update(self, m, id, spam): 779 """ 780 If the current email is a reply to an existing ticket, this function 781 will append the contents of this email to that ticket, instead of 782 creating a new one. 783 """ 784 self.logger.debug("function ticket_update") 785 786 if not self.parameters.ticket_update: 787 self.logger.debug("ticket_update disabled") 788 return False 789 790 ## Must we update ticket fields 791 # 792 update_fields = dict() 793 try: 794 id, keywords = string.split(id, '?') 795 796 update_fields = self.str_to_dict(keywords) 797 798 ## Strip '#' 799 # 800 self.id = int(id[1:]) 801 802 except ValueError: 803 804 ## Strip '#' 805 # 806 self.id = int(id[1:]) 807 808 self.logger.debug("ticket_update id %s" %id) 809 810 ## When is the change committed 811 # 812 if self.VERSION < 0.11: 813 when = int(time.time()) 814 else: 815 when = datetime.now(util.datefmt.utc) 816 817 try: 818 tkt = Ticket(self.env, self.id, self.db) 819 820 except util.TracError, detail: 821 822 ## Not a valid ticket 823 # 824 self.id = None 825 return False 826 827 ## Check the permission of the reporter 828 # 829 if self.parameters.ticket_permission_system: 830 if not self.check_permission(tkt, 'TICKET_APPEND'): 831 self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author) 832 return False 833 834 ## How many changes has this ticket 835 # 836 # cnum = len(tkt.get_changelog()) 837 grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db) 838 cnum = sum(1 for e in grouped) + 1 839 840 841 ## reopen the ticket if it is was closed 842 # We must use the ticket workflow framework 843 # 844 if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11): 845 846 self.logger.debug('Workflow ticket update fields: ') 847 848 from trac.ticket.default_workflow import ConfigurableTicketWorkflow 849 from trac.test import Mock, MockPerm 850 851 req = Mock(authname=self.author, perm=MockPerm(), args={}) 852 try: 853 workflow = self.parameters['workflow_%s' %tkt['status']] 854 except KeyError: 855 ## fallback for compability (Will be deprecated) 856 # 857 if tkt['status'] in ['closed']: 858 workflow = self.parameters.workflow 859 else: 860 workflow = None 861 862 controller = ConfigurableTicketWorkflow(self.env) 863 #print controller.actions 864 #print controller.actions.keys() 865 #print controller.get_ticket_actions(req, tkt) 866 #print controller.actions[workflow] 867 #print controller.actions[workflow]['permissions'] is a list 868 869 if workflow: 870 871 if self.parameters.ticket_permission_system: 872 873 if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]): 874 fields = controller.get_ticket_changes(req, tkt, workflow) 875 else: 876 fields = dict() 877 self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author) 878 879 else: 880 fields = controller.get_ticket_changes(req, tkt, workflow) 881 882 for key in fields.keys(): 883 self.logger.debug('\t %s : %s' %(key, fields[key])) 884 tkt[key] = fields[key] 885 886 ## Old pre 0.11 situation 887 # 888 elif self.parameters.email_triggers_workflow: 889 890 self.logger.debug('email triggers workflow pre trac 0.11') 891 892 if tkt['status'] in ['closed']: 893 tkt['status'] = 'reopened' 894 tkt['resolution'] = '' 895 896 else: 897 self.logger.debug('email triggers workflow disabled') 898 899 ## Must we update some ticket fields properties via subject line 900 # 901 if update_fields: 902 self.update_ticket_fields(tkt, update_fields) 903 904 message_parts = self.get_message_parts(m) 905 message_parts = self.unique_attachment_names(message_parts) 906 907 ## Must we update some ticket fields properties via body_text 908 # 909 if self.properties: 910 self.update_ticket_fields(tkt, self.properties) 911 912 if self.parameters.email_header: 913 message_parts.insert(0, self.email_header_txt(m)) 914 915 body_text = self.get_body_text(message_parts) 916 917 error_with_attachments = self.attach_attachments(message_parts) 918 919 if body_text.strip() or update_fields or self.properties: 920 if self.parameters.dry_run: 921 print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum 922 else: 923 if error_with_attachments: 924 body_text = '%s\\%s' %(error_with_attachments, body_text) 925 self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum)) 926 tkt.save_changes(self.author, body_text, when, None, str(cnum)) 927 928 929 if not spam: 930 self.notify(tkt, False, when) 931 932 return True 933 934 def set_ticket_fields(self, ticket): 935 """ 936 set the ticket fields to value specified 937 - /etc/email2trac.conf with <prefix>_<field> 938 - trac default values, trac.ini 939 """ 940 self.logger.debug('function set_ticket_fields') 941 942 user_dict = dict() 943 944 for field in ticket.fields: 945 946 name = field['name'] 947 948 ## default trac value 949 # 950 if not field.get('custom'): 951 value = self.get_config('ticket', 'default_%s' %(name) ) 952 if (name in ['resolution']) and (value in ['fixed']): 953 value = None 954 else: 955 ## Else get the default value for reporter 956 # 957 value = field.get('value') 958 options = field.get('options') 959 960 if value and options and (value not in options): 961 value = options[int(value)] 962 963 if self.parameters.debug: 964 s = 'trac[%s] = %s' %(name, value) 965 self.print_unicode(s) 966 967 ## email2trac.conf settings 968 # 969 prefix = self.parameters.ticket_prefix 970 try: 971 value = self.parameters['%s_%s' %(prefix, name)] 972 if self.parameters.debug: 973 s = 'email2trac[%s] = %s ' %(name, value) 974 self.print_unicode(s) 975 976 except KeyError, detail: 977 pass 978 979 if value: 980 user_dict[name] = value 981 if self.parameters.debug: 982 s = 'used %s = %s' %(name, value) 983 self.print_unicode(s) 984 985 self.update_ticket_fields(ticket, user_dict, new=1) 986 987 if 'status' not in user_dict.keys(): 988 ticket['status'] = 'new' 989 990 991 def ticket_update_by_subject(self, subject): 992 """ 993 This list of Re: prefixes is probably incomplete. Taken from 994 wikipedia. Here is how the subject is matched 995 - Re: <subject> 996 - Re: (<Mail list label>:)+ <subject> 997 998 So we must have the last column 999 """ 1000 self.logger.debug('function ticket_update_by_subject') 1001 1002 matched_id = None 1003 if self.parameters.ticket_update and self.parameters.ticket_update_by_subject: 1004 1005 SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE) 1006 result = SUBJECT_RE.search(subject) 1007 1008 if result: 1009 ## This is a reply 1010 # 1011 orig_subject = result.group(1) 1012 1013 self.logger.debug('subject search string: %s' %(orig_subject)) 1014 1015 cursor = self.db.cursor() 1016 summaries = [orig_subject, '%%: %s' % orig_subject] 1017 1018 ## Convert days to seconds 1019 # 1020 lookback = int(time.mktime(time.gmtime())) - \ 1021 self.parameters.ticket_update_by_subject_lookback * 24 * 3600 1022 1023 1024 for summary in summaries: 1025 self.logger.debug('Looking for summary matching: "%s"' % summary) 1026 1027 sql = """SELECT id FROM ticket 1028 WHERE changetime >= %s AND summary LIKE %s 1029 ORDER BY changetime DESC""" 1030 cursor.execute(sql, [lookback, summary.strip()]) 1031 1032 for row in cursor: 1033 (matched_id,) = row 1034 1035 self.logger.debug('Found matching ticket id: %d' % matched_id) 1036 1037 break 1038 1039 if matched_id: 1040 matched_id = '#%d' % matched_id 1041 return (matched_id, orig_subject) 1042 1043 subject = orig_subject 1044 1045 return (matched_id, subject) 1046 1047 1048 def new_ticket(self, msg, subject, spam, set_fields = None): 1049 """ 1050 Create a new ticket 1051 """ 1052 self.logger.debug('function new_ticket') 1053 1054 tkt = Ticket(self.env) 1055 1056 self.set_reply_fields(tkt, msg) 1057 1058 self.set_ticket_fields(tkt) 1059 1060 ## Check the permission of the reporter 1061 # 1062 if self.parameters.ticket_permission_system: 1063 if not self.check_permission(tkt, 'TICKET_CREATE'): 1064 self.logger.info('Reporter: %s has no permission to create tickets' %self.author) 1065 return False 1066 1067 ## Old style setting for component, will be removed 1068 # 1069 if spam: 1070 tkt['component'] = 'Spam' 1071 1072 elif self.parameters.has_key('component'): 1073 tkt['component'] = self.parameters['component'] 1074 1075 if not msg['Subject']: 1076 tkt['summary'] = u'(No subject)' 1077 else: 1078 tkt['summary'] = subject 1079 1080 1081 if set_fields: 1082 rest, keywords = string.split(set_fields, '?') 1083 1084 if keywords: 1085 update_fields = self.str_to_dict(keywords) 1086 self.update_ticket_fields(tkt, update_fields) 1087 1088 1089 message_parts = self.get_message_parts(msg) 1090 1091 ## Must we update some ticket fields properties via body_text 1092 # 1093 if self.properties: 1094 self.update_ticket_fields(tkt, self.properties) 1095 1096 message_parts = self.unique_attachment_names(message_parts) 1097 1098 ## produce e-mail like header 1099 # 1100 head = '' 1101 if self.parameters.email_header: 1102 head = self.email_header_txt(msg) 1103 message_parts.insert(0, head) 1104 1105 body_text = self.get_body_text(message_parts) 1106 1107 tkt['description'] = body_text 1108 1109 ## When is the change committed 1110 # 1111 if self.VERSION < 0.11: 1112 when = int(time.time()) 1113 else: 1114 when = datetime.now(util.datefmt.utc) 1115 1116 if self.parameters.dry_run: 1117 print 'DRY_RUN: tkt.insert()' 1118 else: 1119 self.id = tkt.insert() 1120 1121 changed = False 1122 comment = '' 1123 1124 ## some routines in trac are dependend on ticket id 1125 # like alternate notify template 1126 # 1127 if self.parameters.alternate_notify_template: 1128 tkt['id'] = self.id 1129 changed = True 1130 1131 ## Rewrite the description if we have mailto enabled 1132 # 1133 if self.parameters.mailto_link: 1134 changed = True 1135 comment = u'\nadded mailto line\n' 1136 mailto = self.html_mailto_link( m['Subject']) 1137 1138 tkt['description'] = u'%s\r\n%s%s\r\n' \ 1139 %(head, mailto, body_text) 1140 1141 ## Save the attachments to the ticket 1142 # 1143 error_with_attachments = self.attach_attachments(message_parts) 1144 1145 if error_with_attachments: 1146 changed = True 1147 comment = '%s\n%s\n' %(comment, error_with_attachments) 1148 1149 if changed: 1150 if self.parameters.dry_run: 1151 print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author) 1152 else: 1153 tkt.save_changes(tkt['reporter'], comment) 1154 #print tkt.get_changelog(self.db, when) 1155 1156 if not spam: 1157 self.notify(tkt, True) 1158 1159 1160 def attach_attachments(self, message_parts, update=False): 1161 ''' 1162 save any attachments as files in the ticket's directory 1163 ''' 1164 self.logger.debug('function attach_attachments()') 1165 1166 if self.parameters.dry_run: 1167 print "DRY_RUN: no attachments attached to tickets" 1168 return '' 1169 1170 count = 0 1171 1172 ## Get Maxium attachment size 1173 # 1174 max_size = int(self.get_config('attachment', 'max_size')) 1175 status = None 1176 1177 for item in message_parts: 1178 ## Skip body parts 1179 # 1180 if not isinstance(item, tuple): 1181 continue 1182 1183 (original, filename, part) = item 1184 1185 ## We have to determine the size so we use this temporary solution. we must escape it 1186 # else we get UnicodeErrors. 1187 # 1188 path, fd = util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename))) 1189 text = part.get_payload(decode=1) 1190 if not text: 1191 text = '(None)' 1192 fd.write(text) 1193 fd.close() 1194 1195 ## get the file_size 1196 # 1197 stats = os.lstat(path) 1198 file_size = stats[ST_SIZE] 1199 1200 ## Check if the attachment size is allowed 1201 # 1202 if (max_size != -1) and (file_size > max_size): 1203 status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \ 1204 %(status, original, file_size, max_size) 1205 1206 os.unlink(path) 1207 continue 1208 else: 1209 count = count + 1 1210 1211 ## Insert the attachment 1212 # 1213 fd = open(path, 'rb') 1214 if self.system == 'discussion': 1215 att = attachment.Attachment(self.env, 'discussion', 'topic/%s' 1216 % (self.id,)) 1217 else: 1218 self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id)) 1219 att = attachment.Attachment(self.env, 'ticket', self.id) 1220 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1221 ## This will break the ticket_update system, the body_text is vaporized 1222 # ;-( 1223 # 1224 if not update: 1225 att.author = self.author 1226 att.description = self.email_to_unicode('Added by email2trac') 1227 1228 try: 1229 self.logger.debug('Insert atachment') 1230 att.insert(filename, fd, file_size) 1231 except OSError, detail: 1232 self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)) 1233 status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail) 1234 1235 ## Remove the created temporary filename 1236 # 1237 fd.close() 1238 os.unlink(path) 1239 1240 ## return error 1241 # 1242 return status 1243 1243 1244 1244 ########## Fullblog functions ################################################# 1245 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1246 def blog(self, id): 1247 """ 1248 The blog create/update function 1249 """ 1250 ## import the modules 1251 # 1252 from tracfullblog.core import FullBlogCore 1253 from tracfullblog.model import BlogPost, BlogComment 1254 from trac.test import Mock, MockPerm 1255 1256 ## instantiate blog core 1257 # 1258 blog = FullBlogCore(self.env) 1259 req = Mock(authname='anonymous', perm=MockPerm(), args={}) 1260 1261 if id: 1262 1263 ## update blog 1264 # 1265 comment = BlogComment(self.env, id) 1266 comment.author = self.author 1267 1268 message_parts = self.get_message_parts(m) 1269 comment.comment = self.get_body_text(message_parts) 1270 1271 blog.create_comment(req, comment) 1272 1273 else: 1274 ## create blog 1275 # 1276 import time 1277 post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime())) 1278 1279 #post = BlogPost(self.env, blog._get_default_postname(self.env)) 1280 1281 post.author = self.author 1282 post.title = self.email_to_unicode(m['Subject']) 1283 1284 message_parts = self.get_message_parts(m) 1285 post.body = self.get_body_text(message_parts) 1286 1287 blog.create_post(req, post, self.author, u'Created by email2trac', False) 1288 1288 1289 1289 1290 1290 ########## Discussion functions ############################################## 1291 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1292 def discussion_topic(self, content, subject): 1293 1294 ## Import modules. 1295 # 1296 from tracdiscussion.api import DiscussionApi 1297 from trac.util.datefmt import to_timestamp, utc 1298 1299 self.logger.debug('Creating a new topic in forum:', self.id) 1300 1301 ## Get dissussion API component. 1302 # 1303 api = self.env[DiscussionApi] 1304 context = self._create_context(content, subject) 1305 1306 ## Get forum for new topic. 1307 # 1308 forum = api.get_forum(context, self.id) 1309 1310 if not forum: 1311 self.logger.error("ERROR: Replied forum doesn't exist") 1312 1313 ## Prepare topic. 1314 # 1315 topic = {'forum' : forum['id'], 1316 'subject' : context.subject, 1317 'time': to_timestamp(datetime.now(utc)), 1318 'author' : self.author, 1319 'subscribers' : [self.email_addr], 1320 'body' : self.get_body_text(context.content_parts)} 1321 1322 ## Add topic to DB and commit it. 1323 # 1324 self._add_topic(api, context, topic) 1325 self.db.commit() 1326 1327 def discussion_topic_reply(self, content, subject): 1328 1329 ## Import modules. 1330 # 1331 from tracdiscussion.api import DiscussionApi 1332 from trac.util.datefmt import to_timestamp, utc 1333 1334 self.logger.debug('Replying to discussion topic', self.id) 1335 1336 ## Get dissussion API component. 1337 # 1338 api = self.env[DiscussionApi] 1339 context = self._create_context(content, subject) 1340 1341 ## Get replied topic. 1342 # 1343 topic = api.get_topic(context, self.id) 1344 1345 if not topic: 1346 self.logger.error("ERROR: Replied topic doesn't exist") 1347 1348 ## Prepare message. 1349 # 1350 message = {'forum' : topic['forum'], 1351 'topic' : topic['id'], 1352 'replyto' : -1, 1353 'time' : to_timestamp(datetime.now(utc)), 1354 'author' : self.author, 1355 'body' : self.get_body_text(context.content_parts)} 1356 1357 ## Add message to DB and commit it. 1358 # 1359 self._add_message(api, context, message) 1360 self.db.commit() 1361 1362 def discussion_message_reply(self, content, subject): 1363 1364 ## Import modules. 1365 # 1366 from tracdiscussion.api import DiscussionApi 1367 from trac.util.datefmt import to_timestamp, utc 1368 1369 self.logger.debug('Replying to discussion message', self.id) 1370 1371 ## Get dissussion API component. 1372 # 1373 api = self.env[DiscussionApi] 1374 context = self._create_context(content, subject) 1375 1376 ## Get replied message. 1377 # 1378 message = api.get_message(context, self.id) 1379 1380 if not message: 1381 self.logger.error("ERROR: Replied message doesn't exist") 1382 1383 ## Prepare message. 1384 # 1385 message = {'forum' : message['forum'], 1386 'topic' : message['topic'], 1387 'replyto' : message['id'], 1388 'time' : to_timestamp(datetime.now(utc)), 1389 'author' : self.author, 1390 'body' : self.get_body_text(context.content_parts)} 1391 1392 ## Add message to DB and commit it. 1393 # 1394 self._add_message(api, context, message) 1395 self.db.commit() 1396 1397 def _create_context(self, content, subject): 1398 1399 ## Import modules. 1400 # 1401 from trac.mimeview import Context 1402 from trac.web.api import Request 1403 from trac.perm import PermissionCache 1404 1405 ## TODO: Read server base URL from config. 1406 # Create request object to mockup context creation. 1407 # 1408 environ = {'SERVER_PORT' : 80, 1409 'SERVER_NAME' : 'test', 1410 'REQUEST_METHOD' : 'POST', 1411 'wsgi.url_scheme' : 'http', 1412 'wsgi.input' : sys.stdin} 1413 chrome = {'links': {}, 1414 'scripts': [], 1415 'ctxtnav': [], 1416 'warnings': [], 1417 'notices': []} 1418 1419 if self.env.base_url_for_redirect: 1420 environ['trac.base_url'] = self.env.base_url 1421 1422 req = Request(environ, None) 1423 req.chrome = chrome 1424 req.tz = 'missing' 1425 req.authname = self.author 1426 req.perm = PermissionCache(self.env, self.author) 1427 req.locale = None 1428 1429 ## Create and return context. 1430 # 1431 context = Context.from_request(req) 1432 context.realm = 'discussion-email2trac' 1433 context.cursor = self.db.cursor() 1434 context.content = content 1435 context.subject = subject 1436 1437 ## Read content parts from content. 1438 # 1439 context.content_parts = self.get_message_parts(content) 1440 context.content_parts = self.unique_attachment_names( 1441 context.content_parts) 1442 1443 return context 1444 1445 def _add_topic(self, api, context, topic): 1446 context.req.perm.assert_permission('DISCUSSION_APPEND') 1447 1448 ## Filter topic. 1449 # 1450 for discussion_filter in api.discussion_filters: 1451 accept, topic_or_error = discussion_filter.filter_topic( 1452 context, topic) 1453 if accept: 1454 topic = topic_or_error 1455 else: 1456 raise TracError(topic_or_error) 1457 1458 ## Add a new topic. 1459 # 1460 api.add_topic(context, topic) 1461 1462 ## Get inserted topic with new ID. 1463 # 1464 topic = api.get_topic_by_time(context, topic['time']) 1465 1466 ## Attach attachments. 1467 # 1468 self.id = topic['id'] 1469 self.attach_attachments(context.content_parts, True) 1470 1471 ## Notify change listeners. 1472 # 1473 for listener in api.topic_change_listeners: 1474 listener.topic_created(context, topic) 1475 1476 def _add_message(self, api, context, message): 1477 context.req.perm.assert_permission('DISCUSSION_APPEND') 1478 1479 ## Filter message. 1480 # 1481 for discussion_filter in api.discussion_filters: 1482 accept, message_or_error = discussion_filter.filter_message( 1483 context, message) 1484 if accept: 1485 message = message_or_error 1486 else: 1487 raise TracError(message_or_error) 1488 1489 ## Add message. 1490 # 1491 api.add_message(context, message) 1492 1493 ## Get inserted message with new ID. 1494 # 1495 message = api.get_message_by_time(context, message['time']) 1496 1497 ## Attach attachments. 1498 # 1499 self.id = message['topic'] 1500 self.attach_attachments(context.content_parts, True) 1501 1502 ## Notify change listeners. 1503 # 1504 for listener in api.message_change_listeners: 1505 listener.message_created(context, message) 1506 1506 1507 1507 ########## MAIN function ###################################################### 1508 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 if self.parameters.debug:# save the entire e-mail message text1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1509 def parse(self, fp): 1510 """ 1511 """ 1512 self.logger.debug('Main function parse') 1513 global m 1514 1515 m = email.message_from_file(fp) 1516 1517 if not m: 1518 self.logger.debug('This is not a valid email message format') 1519 return 1520 1521 ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696 1522 # 1523 try: 1524 m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', '')) 1525 except AttributeError, detail: 1526 pass 1527 1528 if self.parameters.debug: # save the entire e-mail message text 1529 self.save_email_for_debug(m, True) 1530 1531 self.db = self.env.get_db_cnx() 1532 self.get_sender_info(m) 1533 1534 if not self.email_header_acl('white_list', self.email_addr, True): 1535 self.logger.info('Message rejected : %s not in white list' %(self.email_addr)) 1536 return False 1537 1538 if self.email_header_acl('black_list', self.email_addr, False): 1539 self.logger.info('Message rejected : %s in black list' %(self.email_addr)) 1540 return False 1541 1542 if not self.email_header_acl('recipient_list', self.to_email_addr, True): 1543 self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr)) 1544 return False 1545 1546 ## If spam drop the message 1547 # 1548 if self.spam(m) == 'drop': 1549 return False 1550 1551 elif self.spam(m) == 'spam': 1552 spam_msg = True 1553 else: 1554 spam_msg = False 1555 1556 if not m['Subject']: 1557 subject = 'No Subject' 1558 else: 1559 subject = self.email_to_unicode(m['Subject']) 1560 1561 self.logger.debug('subject: %s' %subject) 1562 1563 ## [hic] #1529: Re: LRZ 1564 # [hic] #1529?owner=bas,priority=medium: Re: LRZ 1565 # 1566 ticket_regex = r''' 1567 (?P<new_fields>[#][?].*) 1568 |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:) 1569 ''' 1570 ## Check if FullBlogPlugin is installed 1571 # 1572 blog_enabled = None 1573 blog_regex = '' 1574 if self.get_config('components', 'tracfullblog.*') in ['enabled']: 1575 self.logger.debug('Trac BLOG support enabled') 1576 blog_enabled = True 1577 blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))''' 1578 1579 1580 ## Check if DiscussionPlugin is installed 1581 # 1582 discussion_enabled = None 1583 discussion_regex = '' 1584 if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']: 1585 self.logger.debug('Trac Discussion support enabled') 1586 discussion_enabled = True 1587 discussion_regex = r''' 1588 |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?) 1589 |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?) 1590 |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?) 1591 ''' 1592 1593 1594 regex_str = ticket_regex + blog_regex + discussion_regex 1595 SYSTEM_RE = re.compile(regex_str, re.VERBOSE) 1596 1597 ## Find out if this is a ticket, a blog or a discussion 1598 # 1599 result = SYSTEM_RE.search(subject) 1600 1601 if result: 1602 ## update ticket + fields 1603 # 1604 if result.group('reply'): 1605 self.system = 'ticket' 1606 1607 ## Skip the last ':' character 1608 # 1609 if not self.ticket_update(m, result.group('reply')[:-1], spam_msg): 1610 self.new_ticket(m, subject, spam_msg) 1611 1612 ## New ticket + fields 1613 # 1614 elif result.group('new_fields'): 1615 self.system = 'ticket' 1616 self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields')) 1617 1618 if blog_enabled: 1619 if result.group('blog'): 1620 self.system = 'blog' 1621 self.blog(result.group('blog_id')) 1622 1623 if discussion_enabled: 1624 ## New topic. 1625 # 1626 if result.group('forum'): 1627 self.system = 'discussion' 1628 self.id = int(result.group('forum_id')) 1629 self.discussion_topic(m, subject[result.end('forum'):]) 1630 1631 ## Reply to topic. 1632 # 1633 elif result.group('topic'): 1634 self.system = 'discussion' 1635 self.id = int(result.group('topic_id')) 1636 self.discussion_topic_reply(m, subject[result.end('topic'):]) 1637 1638 ## Reply to topic message. 1639 # 1640 elif result.group('message'): 1641 self.system = 'discussion' 1642 self.id = int(result.group('message_id')) 1643 self.discussion_message_reply(m, subject[result.end('message'):]) 1644 1645 else: 1646 self.system = 'ticket' 1647 (matched_id, subject) = self.ticket_update_by_subject(subject) 1648 if matched_id: 1649 if not self.ticket_update(m, matched_id, spam_msg): 1650 self.new_ticket(m, subject, spam_msg) 1651 else: 1652 ## No update by subject, so just create a new ticket 1653 # 1654 self.new_ticket(m, subject, spam_msg) 1655 1655 1656 1656 1657 1657 ########## BODY TEXT functions ########################################################### 1658 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1659 def strip_signature(self, text): 1660 """ 1661 Strip signature from message, inspired by Mailman software 1662 """ 1663 self.logger.debug('function strip_signature') 1664 1665 body = [] 1666 for line in text.splitlines(): 1667 if line == '-- ': 1668 break 1669 body.append(line) 1670 1671 return ('\n'.join(body)) 1672 1673 def reflow(self, text, delsp = 0): 1674 """ 1675 Reflow the message based on the format="flowed" specification (RFC 3676) 1676 """ 1677 flowedlines = [] 1678 quotelevel = 0 1679 prevflowed = 0 1680 1681 for line in text.splitlines(): 1682 from re import match 1683 1684 ## Figure out the quote level and the content of the current line 1685 # 1686 m = match('(>*)( ?)(.*)', line) 1687 linequotelevel = len(m.group(1)) 1688 line = m.group(3) 1689 1690 ## Determine whether this line is flowed 1691 # 1692 if line and line != '-- ' and line[-1] == ' ': 1693 flowed = 1 1694 else: 1695 flowed = 0 1696 1697 if flowed and delsp and line and line[-1] == ' ': 1698 line = line[:-1] 1699 1700 ## If the previous line is flowed, append this line to it 1701 # 1702 if prevflowed and line != '-- ' and linequotelevel == quotelevel: 1703 flowedlines[-1] += line 1704 1705 ## Otherwise, start a new line 1706 # 1707 else: 1708 flowedlines.append('>' * linequotelevel + line) 1709 1710 prevflowed = flowed 1711 1712 1713 return '\n'.join(flowedlines) 1714 1715 def strip_quotes(self, text): 1716 """ 1717 Strip quotes from message by Nicolas Mendoza 1718 """ 1719 self.logger.debug('function strip_quotes') 1720 1721 body = [] 1722 for line in text.splitlines(): 1723 try: 1724 1725 if line.startswith(self.parameters.email_quote): 1726 continue 1727 1728 except UnicodeDecodeError: 1729 1730 tmp_line = self.email_to_unicode(line) 1731 if tmp_line.startswith(self.parameters.email_quote): 1732 continue 1733 1734 body.append(line) 1735 1736 return ('\n'.join(body)) 1737 1738 def inline_properties(self, text): 1739 """ 1740 Parse text if we use inline keywords to set ticket fields 1741 """ 1742 self.logger.debug('function inline_properties') 1743 1744 properties = dict() 1745 body = list() 1746 1747 INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$') 1748 1749 for line in text.splitlines(): 1750 match = INLINE_EXP.match(line) 1751 if match: 1752 keyword, value = match.groups() 1753 self.properties[keyword] = value.strip() 1754 1755 self.logger.debug('inline properties: %s : %s' %(keyword,value)) 1756 1757 else: 1758 body.append(line) 1759 1760 return '\n'.join(body) 1761 1762 1763 def wrap_text(self, text, replace_whitespace = False): 1764 """ 1765 Will break a lines longer then given length into several small 1766 lines of size given length 1767 """ 1768 import textwrap 1769 1770 LINESEPARATOR = '\n' 1771 reformat = '' 1772 1773 for s in text.split(LINESEPARATOR): 1774 tmp = textwrap.fill(s, self.parameters.use_textwrap) 1775 if tmp: 1776 reformat = '%s\n%s' %(reformat,tmp) 1777 else: 1778 reformat = '%s\n' %reformat 1779 1780 return reformat 1781 1782 # Python2.4 and higher 1783 # 1784 #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR)) 1785 # 1786 1786 1787 1787 ########## EMAIL attachements functions ########################################################### 1788 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 if not body_text: 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 #try:1980 #filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8')1981 #except TypeError:1982 #pass1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 1789 def inline_part(self, part): 1790 """ 1791 """ 1792 self.logger.debug('function inline_part()') 1793 1794 return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition') 1795 1796 def get_message_parts(self, msg): 1797 """ 1798 parses the email message and returns a list of body parts and attachments 1799 body parts are returned as strings, attachments are returned as tuples of (filename, Message object) 1800 """ 1801 self.logger.debug('function get_message_parts()') 1802 1803 message_parts = list() 1804 1805 ALTERNATIVE_MULTIPART = False 1806 1807 for part in msg.walk(): 1808 content_maintype = part.get_content_maintype() 1809 content_type = part.get_content_type() 1810 1811 self.logger.debug('\t Message part: Main-Type: %s' % content_maintype) 1812 self.logger.debug('\t Message part: Content-Type: %s' % content_type) 1813 1814 ## Check content type 1815 # 1816 if content_type in self.STRIP_CONTENT_TYPES: 1817 self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename())) 1818 continue 1819 1820 ## Catch some mulitpart execptions 1821 # 1822 if content_type == 'multipart/alternative': 1823 ALTERNATIVE_MULTIPART = True 1824 continue 1825 1826 ## Skip multipart containers 1827 # 1828 if content_maintype == 'multipart': 1829 self.logger.debug("\t Skipping multipart container") 1830 continue 1831 1832 ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline" 1833 # 1834 inline = self.inline_part(part) 1835 1836 ## Drop HTML message 1837 # 1838 if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version: 1839 if content_type == 'text/html': 1840 self.logger.debug('\t Skipping alternative HTML message') 1841 ALTERNATIVE_MULTIPART = False 1842 continue 1843 1844 1845 ## Save all non plain text message as attachment 1846 # 1847 if not content_type in ['text/plain']: 1848 1849 if self.parameters.debug: 1850 s = '\t Filename: %s' % part.get_filename() 1851 self.print_unicode(s) 1852 1853 ## First try to use email header function to convert filename. 1854 # If this fails the use the plain filename 1855 # 1856 try: 1857 filename = self.email_to_unicode(part.get_filename()) 1858 except UnicodeEncodeError, detail: 1859 filename = part.get_filename() 1860 1861 message_parts.append((filename, part)) 1862 1863 ## We van only convert html messages 1864 # 1865 if not content_type == 'text/html': 1866 self.logger.debug('\t Appending content_type = %s' %(content_type)) 1867 continue 1868 1869 if not inline: 1870 self.logger.debug('\t Skipping %s, not an inline messsage part' %(content_type)) 1871 continue 1872 1873 ## Try to decode message part. We have a html or plain text messafe 1874 # 1875 body_text = part.get_payload(decode=1) 1876 if not body_text: 1877 body_text = part.get_payload(decode=0) 1878 1879 ## Try to convert html message 1880 # 1881 if content_type == 'text/html': 1882 1883 body_text = self.html_2_txt(body_text) 1884 if not body_text: 1885 continue 1886 1887 format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower() 1888 delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower() 1889 1890 if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed': 1891 body_text = self.reflow(body_text, delsp == 'yes') 1892 1893 if self.parameters.strip_signature: 1894 body_text = self.strip_signature(body_text) 1895 1896 if self.parameters.strip_quotes: 1897 body_text = self.strip_quotes(body_text) 1898 1899 if self.parameters.inline_properties: 1900 body_text = self.inline_properties(body_text) 1901 1902 if self.parameters.use_textwrap: 1903 body_text = self.wrap_text(body_text) 1904 1905 ## Get contents charset (iso-8859-15 if not defined in mail headers) 1906 # 1907 charset = part.get_content_charset() 1908 if not charset: 1909 charset = 'iso-8859-15' 1910 1911 try: 1912 ubody_text = unicode(body_text, charset) 1913 1914 except UnicodeError, detail: 1915 ubody_text = unicode(body_text, 'iso-8859-15') 1916 1917 except LookupError, detail: 1918 ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset) 1919 1920 if self.parameters.verbatim_format: 1921 message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text) 1922 else: 1923 message_parts.append('%s' %ubody_text) 1924 1925 return message_parts 1926 1927 def unique_attachment_names(self, message_parts): 1928 """ 1929 Make sure we have unique names attachments: 1930 - check if it contains illegal characters 1931 - Rename "None" filenames to "untitled-part" 1932 """ 1933 self.logger.debug('function unique_attachment_names()') 1934 renamed_parts = [] 1935 attachment_names = set() 1936 1937 for item in message_parts: 1938 1939 ## If not an attachment, leave it alone 1940 # 1941 if not isinstance(item, tuple): 1942 renamed_parts.append(item) 1943 continue 1944 1945 (filename, part) = item 1946 1947 ## If filename = None, use a default one 1948 # 1949 if filename in [ 'None']: 1950 filename = 'untitled-part' 1951 self.logger.info('Rename filename "None" to: %s' %filename) 1952 1953 ## Guess the extension from the content type, use non strict mode 1954 # some additional non-standard but commonly used MIME types 1955 # are also recognized 1956 # 1957 ext = mimetypes.guess_extension(part.get_content_type(), False) 1958 if not ext: 1959 ext = '.bin' 1960 1961 filename = '%s%s' % (filename, ext) 1962 1963 ## Discard relative paths for windows/unix in attachment names 1964 # 1965 #filename = filename.replace('\\', '/').replace(':', '/') 1966 filename = filename.replace('\\', '_') 1967 filename = filename.replace('/', '_') 1968 1969 ## remove linefeed char 1970 # 1971 for forbidden_char in ['\r', '\n']: 1972 filename = filename.replace(forbidden_char,'') 1973 1974 ## We try to normalize the filename to utf-8 NFC if we can. 1975 # Files uploaded from OS X might be in NFD. 1976 # Check python version and then try it 1977 # 1978 #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3): 1979 # try: 1980 # filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 1981 # except TypeError: 1982 # pass 1983 1984 ## Make the filename unique for this ticket 1985 # 1986 num = 0 1987 unique_filename = filename 1988 dummy_filename, ext = os.path.splitext(filename) 1989 1990 while (unique_filename in attachment_names) or self.attachment_exists(unique_filename): 1991 num += 1 1992 unique_filename = "%s-%s%s" % (dummy_filename, num, ext) 1993 1994 if self.parameters.debug: 1995 s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename) 1996 self.print_unicode(s) 1997 1998 attachment_names.add(unique_filename) 1999 2000 renamed_parts.append((filename, unique_filename, part)) 2001 2002 return renamed_parts 2003 2004 2005 def attachment_exists(self, filename): 2006 2007 if self.parameters.debug: 2008 s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename) 2009 self.print_unicode(s) 2010 2011 ## We have no valid ticket id 2012 # 2013 if not self.id: 2014 return False 2015 2016 try: 2017 if self.system == 'discussion': 2018 att = attachment.Attachment(self.env, 'discussion', 'ticket/%s' 2019 % (self.id,), filename) 2020 else: 2021 att = attachment.Attachment(self.env, 'ticket', self.id, 2022 filename) 2023 return True 2024 except attachment.ResourceNotFound: 2025 return False 2026 2026 2027 2027 ########## TRAC Ticket Text ########################################################### 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 else: 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2028 2029 def get_body_text(self, message_parts): 2030 """ 2031 """ 2032 self.logger.debug('function get_body_text()') 2033 2034 body_text = [] 2035 2036 for part in message_parts: 2037 2038 ## Plain text part, append it 2039 # 2040 if not isinstance(part, tuple): 2041 body_text.extend(part.strip().splitlines()) 2042 body_text.append("") 2043 continue 2044 2045 (original, filename, part) = part 2046 inline = self.inline_part(part) 2047 2048 ## Skip generation of attachment link if html is converted to text 2049 # 2050 if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline: 2051 s = 'Skipping attachment link for html part: %s' %(filename) 2052 self.print_unicode(s) 2053 continue 2054 2055 if part.get_content_maintype() == 'image' and inline: 2056 if self.system != 'discussion': 2057 s = 'wiki image link for: %s' %(filename) 2058 self.print_unicode(s) 2059 body_text.append('[[Image(%s)]]' % filename) 2060 body_text.append("") 2061 else: 2062 if self.system != 'discussion': 2063 s = 'wiki attachment link for: %s' %(filename) 2064 self.print_unicode(s) 2065 body_text.append('[attachment:"%s"]' % filename) 2066 body_text.append("") 2067 2068 ## Convert list body_texts to string 2069 # 2070 body_text = '\r\n'.join(body_text) 2071 return body_text 2072 2073 def html_mailto_link(self, subject): 2074 """ 2075 This function returns a HTML mailto tag with the ticket id and author email address 2076 """ 2077 self.logger.debug("function html_mailto_link") 2078 if not self.author: 2079 author = self.email_addr 2080 else: 2081 author = self.author 2082 2083 if not self.parameters.mailto_cc: 2084 self.parameters.mailto_cc = '' 2085 2086 ## use urllib to escape the chars 2087 # 2088 s = '%s?Subject=%s&Cc=%s' %( 2089 urllib.quote(self.email_addr), 2090 urllib.quote('Re: #%s: %s' %(self.id, subject)), 2091 urllib.quote(self.parameters.mailto_cc) 2092 ) 2093 2094 if self.VERSION in [ 0.10 ]: 2095 s = '\r\n{{{\r\n#!html\r\n<a\r\n href="mailto:%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author) 2096 else: 2097 s = '[mailto:"%s" Reply to: %s]' %(s, author) 2098 2099 self.logger.debug("\tmailto link %s" %s) 2100 return s 2101 2101 2102 2102 ########## TRAC notify section ########################################################### 2103 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2104 def notify(self, tkt, new=True, modtime=0): 2105 """ 2106 A wrapper for the TRAC notify function. So we can use templates 2107 """ 2108 self.logger.debug('function notify()') 2109 2110 if self.parameters.dry_run: 2111 print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter'] 2112 return 2113 try: 2114 2115 #from trac.ticket.web_ui import TicketModule 2116 #from trac.ticket.notification import TicketNotificationSystem 2117 #ticket_sys = TicketNotificationSystem(self.env) 2118 #a = TicketModule(self.env) 2119 #print a.__dict__ 2120 #tn_sys = TicketNotificationSystem(self.env) 2121 #print tn_sys 2122 #print tn_sys.__dict__ 2123 #sys.exit(0) 2124 2125 ## create false {abs_}href properties, to trick Notify() 2126 # 2127 if not (self.VERSION in [0.11, 0.12]): 2128 self.env.abs_href = Href(self.get_config('project', 'url')) 2129 self.env.href = Href(self.get_config('project', 'url')) 2130 2131 2132 tn = TicketNotifyEmail(self.env) 2133 2134 if self.parameters.alternate_notify_template: 2135 2136 if self.VERSION >= 0.11: 2137 2138 from trac.web.chrome import Chrome 2139 2140 if self.parameters.alternate_notify_template_update and not new: 2141 tn.template_name = self.parameters.alternate_notify_template_update 2142 else: 2143 tn.template_name = self.parameters.alternate_notify_template 2144 2145 tn.template = Chrome(tn.env).load_template(tn.template_name, method='text') 2146 2147 else: 2148 2149 tn.template_name = self.parameters.alternate_notify_template 2150 2151 tn.notify(tkt, new, modtime) 2152 2153 except Exception, e: 2154 self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e)) 2155 2155 2156 2156 … … 2159 2159 2160 2160 def ReadConfig(file, name): 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2161 """ 2162 Parse the config file 2163 """ 2164 if not os.path.isfile(file): 2165 print 'File %s does not exist' %file 2166 sys.exit(1) 2167 2168 config = trac_config.Configuration(file) 2169 2170 parentdir = config.get('DEFAULT', 'parentdir') 2171 sections = config.sections() 2172 2173 ## use some trac internals to get the defaults 2174 # 2175 tmp = config.parser.defaults() 2176 project = SaraDict() 2177 2178 for option, value in tmp.items(): 2179 try: 2180 project[option] = int(value) 2181 except ValueError: 2182 project[option] = value 2183 2184 if name: 2185 if name in sections: 2186 project = SaraDict() 2187 for option, value in config.options(name): 2188 try: 2189 project[option] = int(value) 2190 except ValueError: 2191 project[option] = value 2192 2193 elif not parentdir: 2194 print "Not a valid project name: %s, valid names are: %s" %(name, sections) 2195 print "or set parentdir in the [DEFAULT] section" 2196 sys.exit(1) 2197 2198 ## If parentdir then set project dir to parentdir + name 2199 # 2200 if not project.has_key('project'): 2201 if not parentdir: 2202 print "You must set project or parentdir in your configuration file" 2203 sys.exit(1) 2204 elif not name: 2205 print "You must configure a project section in your configuration file" 2206 else: 2207 project['project'] = os.path.join(parentdir, name) 2208 2209 return project 2210 2210 2211 2211 ########## Setup Logging ############################################################### 2212 2212 2213 2213 def setup_log(parameters, project_name, interactive=None): 2214 2215 2216 2217 2214 """ 2215 Setup loging 2216 2217 Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form 2218 2218 would be interpreted by the ConfigParser itself. 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2219 """ 2220 logger = logging.getLogger('email2trac %s' %project_name) 2221 2222 if interactive: 2223 parameters.log_type = 'stderr' 2224 2225 if not parameters.log_type: 2226 if sys.platform in ['win32', 'cygwin']: 2227 paramters.log_type = 'eventlog' 2228 else: 2229 parameters.log_type = 'syslog' 2230 2231 if parameters.log_type == 'file': 2232 2233 if not parameters.log_file: 2234 parameters.log_file = 'email2trac.log' 2235 2236 if not os.path.isabs(parameters.log_file): 2237 parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file) 2238 2239 log_handler = logging.FileHandler(parameters.log_file) 2240 2241 elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'): 2242 ## Requires win32 extensions 2243 # 2244 log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application') 2245 2246 elif parameters.log_type in ('syslog', 'unix'): 2247 log_handler = logging.handlers.SysLogHandler('/dev/log') 2248 2249 elif parameters.log_type in ('stderr'): 2250 log_handler = logging.StreamHandler(sys.stderr) 2251 2252 else: 2253 log_handler = logging.handlers.BufferingHandler(0) 2254 2255 if parameters.log_format: 2256 parameters.log_format = parameters.log_format.replace('$(', '%(') 2257 else: 2258 parameters.log_format = '%(name)s: %(message)s' 2259 2260 log_formatter = logging.Formatter(parameters.log_format) 2261 log_handler.setFormatter(log_formatter) 2262 logger.addHandler(log_handler) 2263 2264 if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0): 2265 logger.setLevel(logging.DEBUG) 2266 parameters.debug = 1 2267 2268 elif parameters.log_level in ['INFO'] or parameters.verbose: 2269 logger.setLevel(logging.INFO) 2270 2271 elif parameters.log_level in ['WARNING']: 2272 logger.setLevel(logging.WARNING) 2273 2274 elif parameters.log_level in ['ERROR']: 2275 logger.setLevel(logging.ERROR) 2276 2277 elif parameters.log_level in ['CRITICAL']: 2278 logger.setLevel(logging.CRITICAL) 2279 2280 else: 2281 logger.setLevel(logging.INFO) 2282 2283 return logger 2284 2284 2285 2285 2286 2286 if __name__ == '__main__': 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2287 ## Default config file 2288 # 2289 configfile = '@email2trac_conf@' 2290 project = '' 2291 component = '' 2292 ticket_prefix = 'default' 2293 dry_run = None 2294 verbose = None 2295 debug_interactive = None 2296 2297 SHORT_OPT = 'cdhf:np:t:v' 2298 LONG_OPT = ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose'] 2299 2300 try: 2301 opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT) 2302 except getopt.error,detail: 2303 print __doc__ 2304 print detail 2305 sys.exit(1) 2306 2307 project_name = None 2308 for opt,value in opts: 2309 if opt in [ '-h', '--help']: 2310 print __doc__ 2311 sys.exit(0) 2312 elif opt in ['-c', '--component']: 2313 component = value 2314 elif opt in ['-d', '--debug']: 2315 debug_interactive = 1 2316 elif opt in ['-f', '--file']: 2317 configfile = value 2318 elif opt in ['-n', '--dry-run']: 2319 dry_run = True 2320 elif opt in ['-p', '--project']: 2321 project_name = value 2322 elif opt in ['-t', '--ticket_prefix']: 2323 ticket_prefix = value 2324 elif opt in ['-v', '--verbose']: 2325 verbose = True 2326 2327 settings = ReadConfig(configfile, project_name) 2328 2329 ## The default prefix for ticket values in email2trac.conf 2330 # 2331 settings.ticket_prefix = ticket_prefix 2332 settings.dry_run = dry_run 2333 settings.verbose = verbose 2334 2335 if not settings.debug and debug_interactive: 2336 settings.debug = debug_interactive 2337 2338 if not settings.project: 2339 print __doc__ 2340 print 'No Trac project is defined in the email2trac config file.' 2341 sys.exit(1) 2342 2343 logger = setup_log(settings, os.path.basename(settings.project), debug_interactive) 2344 2345 if component: 2346 settings['component'] = component 2347 2348 ## Determine major trac version used to be in email2trac.conf 2349 # Quick hack for 0.12 2350 # 2351 version = '0.%s' %(trac_version.split('.')[1]) 2352 if version.startswith('0.12'): 2353 version = '0.12' 2354 2355 logger.debug("Found trac version: %s" %(version)) 2356 2357 try: 2358 if version == '0.10': 2359 from trac import attachment 2360 from trac.env import Environment 2361 from trac.ticket import Ticket 2362 from trac.web.href import Href 2363 from trac import util 2364 from trac.ticket.web_ui import TicketModule 2365 2366 # 2367 # return util.text.to_unicode(str) 2368 # 2369 # see http://projects.edgewall.com/trac/changeset/2799 2370 from trac.ticket.notification import TicketNotifyEmail 2371 from trac import config as trac_config 2372 from trac.core import TracError 2373 2374 elif version == '0.11': 2375 from trac import attachment 2376 from trac.env import Environment 2377 from trac.ticket import Ticket 2378 from trac.web.href import Href 2379 from trac import config as trac_config 2380 from trac import util 2381 from trac.core import TracError 2382 from trac.perm import PermissionSystem 2383 from trac.ticket.web_ui import TicketModule 2384 2385 # 2386 # return util.text.to_unicode(str) 2387 # 2388 # see http://projects.edgewall.com/trac/changeset/2799 2389 from trac.ticket.notification import TicketNotifyEmail 2390 2391 elif version == '0.12': 2392 from trac import attachment 2393 from trac.env import Environment 2394 from trac.ticket import Ticket 2395 from trac.web.href import Href 2396 from trac import config as trac_config 2397 from trac import util 2398 from trac.core import TracError 2399 from trac.perm import PermissionSystem 2400 from trac.ticket.web_ui import TicketModule 2401 2402 # 2403 # return util.text.to_unicode(str) 2404 # 2405 # see http://projects.edgewall.com/trac/changeset/2799 2406 from trac.ticket.notification import TicketNotifyEmail 2407 2408 2409 else: 2410 logger.error('TRAC version %s is not supported' %version) 2411 sys.exit(1) 2412 2413 ## Must be set before environment is created 2414 # 2415 if settings.has_key('python_egg_cache'): 2416 python_egg_cache = str(settings['python_egg_cache']) 2417 os.environ['PYTHON_EGG_CACHE'] = python_egg_cache 2418 2419 if settings.debug > 0: 2420 logger.debug('Loading environment %s', settings.project) 2421 2422 try: 2423 env = Environment(settings['project'], create=0) 2424 except IOError, detail: 2425 print "Trac project does not exists: %s" %(settings['project']) 2426 sys.exit(1) 2427 2428 tktparser = TicketEmailParser(env, settings, logger, float(version)) 2429 tktparser.parse(sys.stdin) 2430 2431 ## Catch all errors and use the logging module 2432 # 2433 except Exception, error: 2434 2435 etype, evalue, etb = sys.exc_info() 2436 for e in traceback.format_exception(etype, evalue, etb): 2437 logger.critical(e) 2438 2439 if m: 2440 tktparser.save_email_for_debug(m, True) 2441 2442 sys.exit(1) 2443 2443 # EOB
Note: See TracChangeset
for help on using the changeset viewer.