Changeset 236


Ignore:
Timestamp:
12/05/08 06:03:24 (15 years ago)
Author:
bromine
Message:

email2trac.py.in:

  • better handling of inline attachments and multiple body parts, closes #105
File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/email2trac.py.in

    r234 r236  
    155155                        os.umask(int(parameters['umask'], 8))
    156156
     157                if parameters.has_key('quote_attachment_filenames'):
     158                        self.QUOTE_ATTACHMENT_FILENAMES = int(parameters['quote_attachment_filenames'])
     159                else:
     160                        self.QUOTE_ATTACHMENT_FILENAMES = 1
     161
    157162                if parameters.has_key('debug'):
    158163                        self.DEBUG = int(parameters['debug'])
     
    355360                return str
    356361
    357         def debug_attachments(self, message):
     362        def debug_body(self, message_body, tempfile=False):
     363                if tempfile:
     364                        import tempfile
     365                        body_file = tempfile.mktemp('.email2trac')
     366                else:
     367                        body_file = os.path.join(self.TMPDIR, 'body.txt')
     368
     369                print 'TD: writing body (%s)' % body_file
     370                fx = open(body_file, 'wb')
     371                if not message_body:
     372                        message_body = '(None)'
     373                fx.write(message_body)
     374                fx.close()
     375                try:
     376                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
     377                except OSError:
     378                        pass
     379
     380        def debug_attachments(self, message_parts):
    358381                n = 0
    359                 for part in message.walk():
    360                         if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
    361                                 print 'TD: multipart container'
     382                for part in message_parts:
     383                        # Skip inline text parts
     384                        if not isinstance(part, tuple):
    362385                                continue
     386                               
     387                        (filename, part) = part
    363388
    364389                        n = n + 1
     
    366391                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
    367392
    368                         if part.is_multipart():
    369                                 print 'TD: this part is multipart'
    370                                 payload = part.get_payload(decode=1)
    371                                 print 'TD: payload:', payload
    372                         else:
    373                                 print 'TD: this part is not multipart'
    374 
    375                         file = 'part%d' %n
    376                         part_file = os.path.join(self.TMPDIR, file)
     393                        part_file = os.path.join(self.TMPDIR, filename)
    377394                        #part_file = '/var/tmp/part%d' % n
    378395                        print 'TD: writing part%d (%s)' % (n,part_file)
     
    643660                        self.update_ticket_fields(tkt, update_fields)
    644661
    645                 body_text = self.get_body_text(m)
     662                message_parts = self.get_message_parts(m)
     663                message_parts = self.unique_attachment_names(message_parts, tkt)
    646664
    647665                if self.EMAIL_HEADER:
    648                         head = self.email_header_txt(m)
    649                         body_text = u"%s\r\n%s" %(head, body_text)
     666                        message_parts.insert(0, self.email_header_txt(m))
     667
     668                body_text = self.body_text(message_parts)
    650669
    651670                if body_text.strip():
     
    655674
    656675                if self.VERSION  == 0.9:
    657                         str = self.attachments(m, tkt, True)
    658                 else:
    659                         str = self.attachments(m, tkt)
     676                        str = self.attachments(message_parts, tkt, True)
     677                else:
     678                        str = self.attachments(message_parts, tkt)
    660679
    661680                if self.notification and not spam:
     
    755774                        head = self.email_header_txt(msg)
    756775                       
    757                 body_text = self.get_body_text(msg)
    758 
    759                 tkt['description'] = '%s\r\n%s' \
    760                         %(head, body_text)
     776                message_parts = self.get_message_parts(msg)
     777                message_parts = self.unique_attachment_names(message_parts)
     778               
     779                if self.EMAIL_HEADER > 0:
     780                        message_parts.insert(0, self.email_header_txt(msg))
     781                       
     782                body_text = self.body_text(message_parts)
     783
     784                tkt['description'] = body_text
    761785
    762786                #when = int(time.time())
     
    785809                                %(head, mailto, body_text)
    786810
    787                 str =  self.attachments(msg, tkt)
     811                str =  self.attachments(message_parts, tkt)
    788812                if str:
    789813                        changed = True
     
    815839
    816840                if self.DEBUG > 1:        # save the entire e-mail message text
     841                        message_parts = self.get_message_parts(m)
     842                        message_parts = self.unique_attachment_names(message_parts)
    817843                        self.save_email_for_debug(m, True)
    818                         self.debug_attachments(m)
     844                        body_text = self.body_text(message_parts)
     845                        self.debug_body(body_text, True)
     846                        self.debug_attachments(message_parts)
    819847
    820848                self.db = self.env.get_db_cnx()
     
    946974
    947975
    948         def get_body_text(self, msg):
    949                 """
    950                 put the message text in the ticket description or in the changes field.
    951                 message text can be plain text or html or something else
    952                 """
    953                 has_description = 0
    954                 encoding = True
    955                 ubody_text = u'No plain text message'
     976        def get_message_parts(self, msg):
     977                """
     978                parses the email message and returns a list of body parts and attachments
     979                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
     980                """
     981                message_parts = []
     982
    956983                for part in msg.walk():
    957 
    958984                        # 'multipart/*' is a container for multipart messages
    959985                        #
     986                        if self.DEBUG:
     987                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
     988
    960989                        if part.get_content_maintype() == 'multipart':
    961990                                continue
    962991
    963                         if part.get_content_type() == 'text/plain':
     992                        # 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"
     993                        inline = self.inline_part(part)
     994
     995                        # Inline text parts are where the body is
     996                        if part.get_content_type() == 'text/plain' and inline:
     997                                if self.DEBUG:
     998                                        print 'TD:               Inline body part'
     999
    9641000                                # Try to decode, if fails then do not decode
    9651001                                #
     
    9981034                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
    9991035
    1000                         elif part.get_content_type() == 'text/html':
    1001                                 ubody_text = '(see attachment for HTML mail message)'
    1002 
     1036                                if self.VERBATIM_FORMAT:
     1037                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
     1038                                else:
     1039                                        message_parts.append('%s' %ubody_text)
    10031040                        else:
    1004                                 ubody_text = '(see attachment for message)'
    1005 
    1006                         has_description = 1
    1007                         break           # we have the description, so break
    1008 
    1009                 if not has_description:
    1010                         ubody_text = '(see attachment for message)'
    1011 
    1012                 # A patch so that the web-interface will not update the description
    1013                 # field of a ticket
    1014                 #
    1015                 ubody_text = ('\r\n'.join(ubody_text.splitlines()))
    1016 
    1017                 #  If we can unicode it try to encode it for trac
    1018                 #  else we a lot of garbage
    1019                 #
    1020                 #if encoding:
    1021                 #       ubody_text = ubody_text.encode('utf-8')
    1022 
    1023                 if self.VERBATIM_FORMAT:
    1024                         ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
    1025                 else:
    1026                         ubody_text = '%s' %ubody_text
    1027 
    1028                 return ubody_text
     1041                                if self.DEBUG:
     1042                                        print 'TD:               Filename: %s' % part.get_filename()
     1043
     1044                                message_parts.append((part.get_filename(), part))
     1045
     1046                return message_parts
     1047               
     1048        def unique_attachment_names(self, message_parts, tkt = None):
     1049                renamed_parts = []
     1050                attachment_names = set()
     1051                for part in message_parts:
     1052                       
     1053                        # If not an attachment, leave it alone
     1054                        if not isinstance(part, tuple):
     1055                                renamed_parts.append(part)
     1056                                continue
     1057                               
     1058                        (filename, part) = part
     1059                        # Decode the filename
     1060                        if filename:
     1061                                filename = self.email_to_unicode(filename)                     
     1062                        # If no name, use a default one
     1063                        else:
     1064                                filename = 'untitled-part'
     1065
     1066                                # Guess the extension from the content type
     1067                                ext = mimetypes.guess_extension(part.get_content_type())
     1068                                if not ext:
     1069                                        ext = '.bin'
     1070
     1071                                filename = '%s%s' % (filename, ext)
     1072
     1073                        # Discard relative paths in attachment names
     1074                        filename = filename.replace('\\', '/').replace(':', '/')
     1075                        filename = os.path.basename(filename)
     1076
     1077                        # We try to normalize the filename to utf-8 NFC if we can.
     1078                        # Files uploaded from OS X might be in NFD.
     1079                        # Check python version and then try it
     1080                        #
     1081                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
     1082                                try:
     1083                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
     1084                                except TypeError:
     1085                                        pass
     1086                                       
     1087                        if self.QUOTE_ATTACHMENT_FILENAMES:
     1088                                filename = urllib.quote(filename)
     1089
     1090                        # Make the filename unique for this ticket
     1091                        num = 0
     1092                        unique_filename = filename
     1093                        filename, ext = os.path.splitext(filename)
     1094
     1095                        while unique_filename in attachment_names or self.attachment_exists(tkt, unique_filename):
     1096                                num += 1
     1097                                unique_filename = "%s-%s%s" % (filename, num, ext)
     1098                               
     1099                        if self.DEBUG:
     1100                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
     1101
     1102                        attachment_names.add(unique_filename)
     1103
     1104                        renamed_parts.append((filename, unique_filename, part))
     1105               
     1106                return renamed_parts
     1107                       
     1108        def inline_part(self, part):
     1109                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
     1110               
     1111                       
     1112        def attachment_exists(self, tkt, filename):
     1113                if tkt is None:
     1114                        return False
     1115                       
     1116                try:
     1117                        Attachment(self.env, 'ticket', tkt['id'], filename)
     1118                        return True
     1119                except ResourceNotFound:
     1120                        return False
     1121                       
     1122        def body_text(self, message_parts):
     1123                body_text = []
     1124               
     1125                for part in message_parts:
     1126                        # Plain text part, append it
     1127                        if not isinstance(part, tuple):
     1128                                body_text.extend(part.strip().splitlines())
     1129                                body_text.append("")
     1130                                continue
     1131                               
     1132                        (original, filename, part) = part
     1133                        inline = self.inline_part(part)
     1134                       
     1135                        if part.get_content_maintype() == 'image' and inline:
     1136                                body_text.append('[[Image(%s)]]' % filename)
     1137                                body_text.append("")
     1138                        else:
     1139                                body_text.append('[attachment:"%s"]' % filename)
     1140                                body_text.append("")
     1141                               
     1142                body_text = '\r\n'.join(body_text)
     1143                return body_text
    10291144
    10301145        def notify(self, tkt , new=True, modtime=0):
     
    10871202                return str
    10881203
    1089         def attachments(self, message, ticket, update=False):
     1204        def attachments(self, message_parts, ticket, update=False):
    10901205                '''
    10911206                save any attachments as files in the ticket's directory
     
    10991214                max_size = int(self.get_config('attachment', 'max_size'))
    11001215                status   = ''
    1101 
    1102                 for part in message.walk():
    1103                         if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
     1216               
     1217                for part in message_parts:
     1218                        # Skip body parts
     1219                        if not isinstance(part, tuple):
    11041220                                continue
    1105 
    1106                         if not first:                                                                           # first content is the message
    1107                                 first = 1
    1108                                 if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
    1109                                         continue
    1110 
    1111                         filename = part.get_filename()
    1112                         if not filename:
    1113                                 number = number + 1
    1114                                 filename = 'part%04d' % number
    1115 
    1116                                 ext = mimetypes.guess_extension(part.get_content_type())
    1117                                 if not ext:
    1118                                         ext = '.bin'
    1119 
    1120                                 filename = '%s%s' % (filename, ext)
    1121                         else:
    1122                                 filename = self.email_to_unicode(filename)
    1123 
    1124                         # From the trac code
    1125                         #
    1126                         filename = filename.replace('\\', '/').replace(':', '/')
    1127                         filename = os.path.basename(filename)
    1128 
    1129                         # We try to normalize the filename to utf-8 NFC if we can.
    1130                         # Files uploaded from OS X might be in NFD.
    1131                         # Check python version and then try it
    1132                         #
    1133                         if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
    1134                                 try:
    1135                                         filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
    1136                                 except TypeError:
    1137                                         pass
    1138 
    1139                         url_filename = urllib.quote(filename)
     1221                               
     1222                        (original, filename, part) = part
    11401223                        #
    11411224                        # Must be tuneables HvB
    11421225                        #
    1143                         path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
     1226                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
    11441227                        text = part.get_payload(decode=1)
    11451228                        if not text:
     
    11571240                        if (max_size != -1) and (file_size > max_size):
    11581241                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
    1159                                         %(status, filename, file_size, max_size)
     1242                                        %(status, original, file_size, max_size)
    11601243
    11611244                                os.unlink(path)
     
    11761259                                att.description = self.email_to_unicode('Added by email2trac')
    11771260
    1178                         att.insert(url_filename, fd, file_size)
     1261                        att.insert(filename, fd, file_size)
    11791262                        #except  util.TracError, detail:
    11801263                        #       print detail
Note: See TracChangeset for help on using the changeset viewer.