Ticket #105: inline-attachments.diff

File inline-attachments.diff, 12.1 KB (added by ben@…, 15 years ago)
  • private/var/folders/2l/2l213h3uF+8i8q5g7V96LE+++TI/Cleanup

     
    154154                if parameters.has_key('umask'):
    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'])
    159164                else:
     
    354359                #str = str.encode('utf-8')
    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
    365390                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
    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)
    379396                        fx = open(part_file, 'wb')
     
    642659                if update_fields:
    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():
    652671                        tkt.save_changes(self.author, body_text, when)
     
    654673                tkt['id'] = ticket_id
    655674
    656675                if self.VERSION  == 0.9:
    657                         str = self.attachments(m, tkt, True)
     676                        str = self.attachments(message_parts, tkt, True)
    658677                else:
    659                         str = self.attachments(m, tkt)
     678                        str = self.attachments(message_parts, tkt)
    660679
    661680                if self.notification and not spam:
    662681                        self.notify(tkt, False, when)
     
    753772                if self.EMAIL_HEADER > 0:
    754773                        head = self.email_header_txt(msg)
    755774                       
    756                 body_text = self.get_body_text(msg)
     775                message_parts = self.get_message_parts(msg)
     776                message_parts = self.unique_attachment_names(message_parts)
     777               
     778                if self.EMAIL_HEADER > 0:
     779                        message_parts.insert(0, self.email_header_txt(msg))
     780                       
     781                body_text = self.body_text(message_parts)
    757782
    758                 tkt['description'] = '%s\r\n%s' \
    759                         %(head, body_text)
     783                tkt['description'] = body_text
    760784
    761785                #when = int(time.time())
    762786                #
     
    783807                        tkt['description'] = u'%s\r\n%s%s\r\n' \
    784808                                %(head, mailto, body_text)
    785809
    786                 str =  self.attachments(msg, tkt)
     810                str =  self.attachments(message_parts, tkt)
    787811                if str:
    788812                        changed = True
    789813                        comment = '%s\n%s\n' %(comment, str)
     
    813837                        return
    814838
    815839                if self.DEBUG > 1:        # save the entire e-mail message text
     840                        message_parts = self.get_message_parts(m)
     841                        message_parts = self.unique_attachment_names(message_parts)
    816842                        self.save_email_for_debug(m, True)
    817                         self.debug_attachments(m)
     843                        body_text = self.body_text(message_parts)
     844                        self.debug_body(body_text, True)
     845                        self.debug_attachments(message_parts)
    818846
    819847                self.db = self.env.get_db_cnx()
    820848                self.get_sender_info(m)
     
    944972                #
    945973
    946974
    947         def get_body_text(self, msg):
     975        def get_message_parts(self, msg):
    948976                """
    949                 put the message text in the ticket description or in the changes field.
    950                 message text can be plain text or html or something else
     977                parses the email message and returns a list of body parts and attachments
     978                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
    951979                """
    952                 has_description = 0
    953                 encoding = True
    954                 ubody_text = u'No plain text message'
    955                 for part in msg.walk():
     980                message_parts = []
    956981
     982                for part in msg.walk():
    957983                        # 'multipart/*' is a container for multipart messages
    958984                        #
     985                        if self.DEBUG:
     986                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
     987
    959988                        if part.get_content_maintype() == 'multipart':
    960989                                continue
    961990
    962                         if part.get_content_type() == 'text/plain':
     991                        # 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"
     992                        inline = self.inline_part(part)
     993
     994                        # Inline text parts are where the body is
     995                        if part.get_content_type() == 'text/plain' and inline:
     996                                if self.DEBUG:
     997                                        print 'TD:               Inline body part'
     998
    963999                                # Try to decode, if fails then do not decode
    9641000                                #
    9651001                                body_text = part.get_payload(decode=1)
     
    9961032                                except LookupError, detail:
    9971033                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
    9981034
    999                         elif part.get_content_type() == 'text/html':
    1000                                 ubody_text = '(see attachment for HTML mail message)'
     1035                                if self.VERBATIM_FORMAT:
     1036                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
     1037                                else:
     1038                                        message_parts.append('%s' %ubody_text)
     1039                        else:
     1040                                if self.DEBUG:
     1041                                        print 'TD:               Filename: %s' % part.get_filename()
     1042
     1043                                message_parts.append((part.get_filename(), part))
    10011044
     1045                return message_parts
     1046               
     1047        def unique_attachment_names(self, message_parts, tkt = None):
     1048                renamed_parts = []
     1049                attachment_names = set()
     1050                for part in message_parts:
     1051                       
     1052                        # If not an attachment, leave it alone
     1053                        if not isinstance(part, tuple):
     1054                                renamed_parts.append(part)
     1055                                continue
     1056                               
     1057                        (filename, part) = part
     1058                        # Decode the filename
     1059                        if filename:
     1060                                filename = self.email_to_unicode(filename)                     
     1061                        # If no name, use a default one
    10021062                        else:
    1003                                 ubody_text = '(see attachment for message)'
     1063                                filename = 'untitled-part'
    10041064
    1005                         has_description = 1
    1006                         break           # we have the description, so break
     1065                                # Guess the extension from the content type
     1066                                ext = mimetypes.guess_extension(part.get_content_type())
     1067                                if not ext:
     1068                                        ext = '.bin'
    10071069
    1008                 if not has_description:
    1009                         ubody_text = '(see attachment for message)'
     1070                                filename = '%s%s' % (filename, ext)
    10101071
    1011                 # A patch so that the web-interface will not update the description
    1012                 # field of a ticket
    1013                 #
    1014                 ubody_text = ('\r\n'.join(ubody_text.splitlines()))
     1072                        # Discard relative paths in attachment names
     1073                        filename = filename.replace('\\', '/').replace(':', '/')
     1074                        filename = os.path.basename(filename)
    10151075
    1016                 #  If we can unicode it try to encode it for trac
    1017                 #  else we a lot of garbage
    1018                 #
    1019                 #if encoding:
    1020                 #       ubody_text = ubody_text.encode('utf-8')
     1076                        # We try to normalize the filename to utf-8 NFC if we can.
     1077                        # Files uploaded from OS X might be in NFD.
     1078                        # Check python version and then try it
     1079                        #
     1080                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
     1081                                try:
     1082                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
     1083                                except TypeError:
     1084                                        pass
     1085                                       
     1086                        if self.QUOTE_ATTACHMENT_FILENAMES:
     1087                                filename = urllib.quote(filename)
    10211088
    1022                 if self.VERBATIM_FORMAT:
    1023                         ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
    1024                 else:
    1025                         ubody_text = '%s' %ubody_text
     1089                        # Make the filename unique for this ticket
     1090                        num = 0
     1091                        unique_filename = filename
     1092                        filename, ext = os.path.splitext(filename)
     1093
     1094                        while unique_filename in attachment_names or self.attachment_exists(tkt, unique_filename):
     1095                                num += 1
     1096                                unique_filename = "%s-%s%s" % (filename, num, ext)
     1097                               
     1098                        if self.DEBUG:
     1099                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
     1100
     1101                        attachment_names.add(unique_filename)
    10261102
    1027                 return ubody_text
     1103                        renamed_parts.append((filename, unique_filename, part))
     1104               
     1105                return renamed_parts
     1106                       
     1107        def inline_part(self, part):
     1108                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
     1109               
     1110                       
     1111        def attachment_exists(self, tkt, filename):
     1112                if tkt is None:
     1113                        return False
     1114                       
     1115                try:
     1116                        Attachment(self.env, 'ticket', tkt['id'], filename)
     1117                        return True
     1118                except ResourceNotFound:
     1119                        return False
     1120                       
     1121        def body_text(self, message_parts):
     1122                body_text = []
     1123               
     1124                for part in message_parts:
     1125                        # Plain text part, append it
     1126                        if not isinstance(part, tuple):
     1127                                body_text.extend(part.strip().splitlines())
     1128                                body_text.append("")
     1129                                continue
     1130                               
     1131                        (original, filename, part) = part
     1132                        inline = self.inline_part(part)
     1133                       
     1134                        if part.get_content_maintype() == 'image' and inline:
     1135                                body_text.append('[[Image(%s)]]' % filename)
     1136                                body_text.append("")
     1137                        else:
     1138                                body_text.append('[attachment:"%s"]' % filename)
     1139                                body_text.append("")
     1140                               
     1141                body_text = '\r\n'.join(body_text)
     1142                return body_text
    10281143
    10291144        def notify(self, tkt , new=True, modtime=0):
    10301145                """
     
    10851200                str = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(str, author)
    10861201                return str
    10871202
    1088         def attachments(self, message, ticket, update=False):
     1203        def attachments(self, message_parts, ticket, update=False):
    10891204                '''
    10901205                save any attachments as files in the ticket's directory
    10911206                '''
     
    10971212                #
    10981213                max_size = int(self.get_config('attachment', 'max_size'))
    10991214                status   = ''
    1100 
    1101                 for part in message.walk():
    1102                         if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
     1215               
     1216                for part in message_parts:
     1217                        # Skip body parts
     1218                        if not isinstance(part, tuple):
    11031219                                continue
    1104 
    1105                         if not first:                                                                           # first content is the message
    1106                                 first = 1
    1107                                 if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
    1108                                         continue
    1109 
    1110                         filename = part.get_filename()
    1111                         if not filename:
    1112                                 number = number + 1
    1113                                 filename = 'part%04d' % number
    1114 
    1115                                 ext = mimetypes.guess_extension(part.get_content_type())
    1116                                 if not ext:
    1117                                         ext = '.bin'
    1118 
    1119                                 filename = '%s%s' % (filename, ext)
    1120                         else:
    1121                                 filename = self.email_to_unicode(filename)
    1122 
    1123                         # From the trac code
    1124                         #
    1125                         filename = filename.replace('\\', '/').replace(':', '/')
    1126                         filename = os.path.basename(filename)
    1127 
    1128                         # We try to normalize the filename to utf-8 NFC if we can.
    1129                         # Files uploaded from OS X might be in NFD.
    1130                         # Check python version and then try it
    1131                         #
    1132                         if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
    1133                                 try:
    1134                                         filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
    1135                                 except TypeError:
    1136                                         pass
    1137 
    1138                         url_filename = urllib.quote(filename)
     1220                               
     1221                        (original, filename, part) = part
    11391222                        #
    11401223                        # Must be tuneables HvB
    11411224                        #
    1142                         path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
     1225                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
    11431226                        text = part.get_payload(decode=1)
    11441227                        if not text:
    11451228                                text = '(None)'
     
    11551238                        #
    11561239                        if (max_size != -1) and (file_size > max_size):
    11571240                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
    1158                                         %(status, filename, file_size, max_size)
     1241                                        %(status, original, file_size, max_size)
    11591242
    11601243                                os.unlink(path)
    11611244                                continue
     
    11741257                                att.author = self.author
    11751258                                att.description = self.email_to_unicode('Added by email2trac')
    11761259
    1177                         att.insert(url_filename, fd, file_size)
     1260                        att.insert(filename, fd, file_size)
    11781261                        #except  util.TracError, detail:
    11791262                        #       print detail
    11801263