source: emailtotracscript/trunk/email2trac.py.in @ 156

Last change on this file since 156 was 155, checked in by bas, 17 years ago

EmailToTracScript?
EmailtoTracScript?:

email2trac.py.in:

  • Added line for only email address instead of name <x@…>
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 24.2 KB
RevLine 
[22]1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
30Changed By: Bas van der Vlies <basv@sara.nl>
31Date      : 13 September 2005
32Descr.    : Added config file and command line options, spam level
33            detection, reply address and mailto option. Unicode support
34
35Changed By: Walter de Jong <walter@sara.nl>
36Descr.    : multipart-message code and trac attachments
37
38
39The scripts reads emails from stdin and inserts directly into a Trac database.
40MIME headers are mapped as follows:
41
42        * From:      => Reporter
[152]43                     => CC (Optional via reply_all option)
[22]44        * Subject:   => Summary
45        * Body       => Description
46        * Component  => Can be set to SPAM via spam_level option
47
48How to use
49----------
50 * Create an config file:
[74]51        [DEFAULT]                      # REQUIRED
52        project      : /data/trac/test # REQUIRED
53        debug        : 1               # OPTIONAL, if set print some DEBUG info
54        spam_level   : 4               # OPTIONAL, if set check for SPAM mail
[152]55        reply_all    : 1               # OPTIONAL, if set then fill in ticket CC field
[87]56        umask        : 022             # OPTIONAL, if set then use this umask for creation of the attachments
[74]57        mailto_link  : 1               # OPTIONAL, if set then [mailto:<>] in description
[75]58        mailto_cc    : basv@sara.nl    # OPTIONAL, use this address as CC in mailto line
[74]59        ticket_update: 1               # OPTIONAL, if set then check if this is an update for a ticket
[148]60        trac_version : 0.8             # OPTIONAL, default is 0.10
[22]61
[148]62        [jouvin]                       # OPTIONAL project declaration, if set both fields necessary
[22]63        project      : /data/trac/jouvin # use -p|--project jouvin. 
64       
65 * default config file is : /etc/email2trac.conf
66
67 * Commandline opions:
68                -h | --help
69                -c <value> | --component=<value>
70                -f <config file> | --file=<config file>
71                -p <project name> | --project=<project name>
72
73SVN Info:
74        $Id: email2trac.py.in 155 2007-04-24 10:51:43Z bas $
75"""
76import os
77import sys
78import string
79import getopt
80import stat
81import time
82import email
[136]83import email.Iterators
84import email.Header
[22]85import re
86import urllib
87import unicodedata
88import ConfigParser
89from stat import *
90import mimetypes
[96]91import syslog
92import traceback
[22]93
[91]94
[96]95# Some global variables
96#
[146]97trac_default_version = 0.10
[96]98m = None
[22]99
[96]100
[22]101class TicketEmailParser(object):
102        env = None
103        comment = '> '
104   
105        def __init__(self, env, parameters, version):
106                self.env = env
107
108                # Database connection
109                #
110                self.db = None
111
[72]112                # Some useful mail constants
113                #
114                self.author = None
115                self.email_addr = None
116                self.email_field = None
117
[22]118                self.VERSION = version
[68]119                if self.VERSION == 0.8:
120                        self.get_config = self.env.get_config
121                else:
[22]122                        self.get_config = self.env.config.get
123
124                if parameters.has_key('umask'):
125                        os.umask(int(parameters['umask'], 8))
126
127                if parameters.has_key('debug'):
128                        self.DEBUG = int(parameters['debug'])
129                else:
130                        self.DEBUG = 0
131
132                if parameters.has_key('mailto_link'):
133                        self.MAILTO = int(parameters['mailto_link'])
[74]134                        if parameters.has_key('mailto_cc'):
135                                self.MAILTO_CC = parameters['mailto_cc']
136                        else:
137                                self.MAILTO_CC = ''
[22]138                else:
139                        self.MAILTO = 0
140
141                if parameters.has_key('spam_level'):
142                        self.SPAM_LEVEL = int(parameters['spam_level'])
143                else:
144                        self.SPAM_LEVEL = 0
145
146                if parameters.has_key('email_comment'):
147                        self.comment = str(parameters['email_comment'])
148
149                if parameters.has_key('email_header'):
150                        self.EMAIL_HEADER = int(parameters['email_header'])
151                else:
152                        self.EMAIL_HEADER = 0
153
[42]154                if parameters.has_key('alternate_notify_template'):
155                        self.notify_template = str(parameters['alternate_notify_template'])
156                else:
157                        self.notify_template = None
[22]158
[43]159                if parameters.has_key('reply_all'):
160                        self.REPLY_ALL = int(parameters['reply_all'])
161                else:
162                        self.REPLY_ALL = 0
[42]163
[74]164                if parameters.has_key('ticket_update'):
165                        self.TICKET_UPDATE = int(parameters['ticket_update'])
166                else:
167                        self.TICKET_UPDATE = 0
[43]168
[118]169                if parameters.has_key('drop_spam'):
170                        self.DROP_SPAM = int(parameters['drop_spam'])
171                else:
172                        self.DROP_SPAM = 0
[74]173
[134]174                if parameters.has_key('verbatim_format'):
175                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
176                else:
177                        self.VERBATIM_FORMAT = 1
[118]178
[136]179                if parameters.has_key('strip_signature'):
180                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
181                else:
182                        self.STRIP_SIGNATURE = 0
[134]183
[148]184                if parameters.has_key('use_textwrap'):
185                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
186                else:
187                        self.USE_TEXTWRAP = 0
188
[22]189        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
190        # Note if Spam_level then '*' are included
191        def spam(self, message):
192                if message.has_key('X-Spam-Score'):
193                        spam_l = string.split(message['X-Spam-Score'])
194                        number = spam_l[0].count('*')
195
196                        if number >= self.SPAM_LEVEL:
[41]197                                return 'Spam'
[22]198
[67]199                elif message.has_key('X-Virus-found'):                  # treat virus mails as spam
200                        return 'Spam'
201
[41]202                return self.get_config('ticket', 'default_component')
[22]203
[149]204        def blacklisted_from(self):
205                FROM_RE = re.compile(r"""
206                    MAILER-DAEMON@
207                    """, re.VERBOSE)
208                result =  FROM_RE.search(self.email_addr)
209                if result:
210                        return True
211                else:
212                        return False
213
[139]214        def email_to_unicode(self, message_str):
[22]215                """
216                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]217        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]218                understands it.
219                """
[139]220                results =  email.Header.decode_header(message_str)
[22]221                str = None
222                for text,format in results:
223                        if format:
224                                try:
225                                        temp = unicode(text, format)
[139]226                                except UnicodeError, detail:
[22]227                                        # This always works
228                                        #
229                                        temp = unicode(text, 'iso-8859-15')
[139]230                                except LookupError, detail:
231                                        #text = 'ERROR: Could not find charset: %s, please install' %format
232                                        #temp = unicode(text, 'iso-8859-15')
233                                        temp = message_str
234                                       
[22]235                        else:
236                                temp = string.strip(text)
[92]237                                temp = unicode(text, 'iso-8859-15')
[22]238
239                        if str:
[100]240                                str = '%s %s' %(str, temp)
[22]241                        else:
[100]242                                str = '%s' %temp
[22]243
[139]244                #str = str.encode('utf-8')
[22]245                return str
246
247        def debug_attachments(self, message):
248                n = 0
249                for part in message.walk():
250                        if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
251                                print 'TD: multipart container'
252                                continue
253
254                        n = n + 1
255                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
256                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
257
258                        if part.is_multipart():
259                                print 'TD: this part is multipart'
260                                payload = part.get_payload(decode=1)
261                                print 'TD: payload:', payload
262                        else:
263                                print 'TD: this part is not multipart'
264
265                        part_file = '/var/tmp/part%d' % n
266                        print 'TD: writing part%d (%s)' % (n,part_file)
267                        fx = open(part_file, 'wb')
268                        text = part.get_payload(decode=1)
269                        if not text:
270                                text = '(None)'
271                        fx.write(text)
272                        fx.close()
273                        try:
274                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
275                        except OSError:
276                                pass
277
278        def email_header_txt(self, m):
[72]279                """
280                Display To and CC addresses in description field
281                """
[22]282                str = ''
283                if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
284                        str = "'''To:''' %s [[BR]]" %(m['To'])
285                if m['Cc'] and len(m['Cc']) > 0:
286                        str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc'])
287
[139]288                return  self.email_to_unicode(str)
[22]289
[138]290
[43]291        def set_owner(self, ticket):
[45]292                """
293                Select default owner for ticket component
294                """
[43]295                cursor = self.db.cursor()
296                sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
297                cursor.execute(sql)
[61]298                try:
299                        ticket['owner'] = cursor.fetchone()[0]
300                except TypeError, detail:
301                        ticket['owner'] = "UNKNOWN"
[43]302
[72]303        def get_author_emailaddrs(self, message):
[45]304                """
[72]305                Get the default author name and email address from the message
[45]306                """
[143]307                temp = self.email_to_unicode(message['from'])
308                #print temp.encode('utf-8')
[43]309
[143]310                self.author, self.email_addr  = email.Utils.parseaddr(temp)
311                #print self.author.encode('utf-8', 'replace')
[142]312
[45]313                # Look for email address in registered trac users
314                #
[68]315                if self.VERSION == 0.8:
316                        users = []
317                else:
[45]318                        users = [ u for (u, n, e) in self.env.get_known_users(self.db)
[72]319                                if e == self.email_addr ]
[43]320
[45]321                if len(users) == 1:
[72]322                        self.email_field = users[0]
[45]323                else:
[139]324                        self.email_field =  self.email_to_unicode(message['from'])
[155]325                        #self.email_field =  self.email_to_unicode(self.email_addr)
[45]326
[72]327        def set_reply_fields(self, ticket, message):
328                """
329                Set all the right fields for a new ticket
330                """
331                ticket['reporter'] = self.email_field
332
[45]333                # Put all CC-addresses in ticket CC field
[43]334                #
335                if self.REPLY_ALL:
[45]336                        #tos = message.get_all('to', [])
[43]337                        ccs = message.get_all('cc', [])
338
[45]339                        addrs = email.Utils.getaddresses(ccs)
[105]340                        if not addrs:
341                                return
[43]342
343                        # Remove reporter email address if notification is
344                        # on
345                        #
346                        if self.notification:
347                                try:
[72]348                                        addrs.remove((self.author, self.email_addr))
[43]349                                except ValueError, detail:
350                                        pass
351
[45]352                        for name,mail in addrs:
[105]353                                try:
[108]354                                        mail_list = '%s, %s' %(mail_list, mail)
[105]355                                except:
356                                        mail_list = mail
[43]357
[105]358                        if mail_list:
[139]359                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]360
361        def save_email_for_debug(self, message, tempfile=False):
362                if tempfile:
363                        import tempfile
364                        msg_file = tempfile.mktemp('.email2trac')
365                else:
366                        msg_file = '/var/tmp/msg.txt'
[44]367                print 'TD: saving email to %s' % msg_file
368                fx = open(msg_file, 'wb')
369                fx.write('%s' % message)
370                fx.close()
371                try:
372                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
373                except OSError:
374                        pass
375
[71]376        def ticket_update(self, m):
[78]377                """
[79]378                If the current email is a reply to an existing ticket, this function
379                will append the contents of this email to that ticket, instead of
380                creating a new one.
[78]381                """
[71]382                if not m['Subject']:
383                        return False
384                else:
[139]385                        subject  = self.email_to_unicode(m['Subject'])
[71]386
387                TICKET_RE = re.compile(r"""
388                                        (?P<ticketnr>[#][0-9]+:)
389                                        """, re.VERBOSE)
390
391                result =  TICKET_RE.search(subject)
392                if not result:
393                        return False
394
[78]395                body_text = self.get_body_text(m)
396
[72]397                # Strip '#' and ':' from ticket_id
[75]398                #
[71]399                ticket_id = result.group('ticketnr')
400                ticket_id = int(ticket_id[1:-1])
401
[77]402                # Get current time
403                #
404                when = int(time.time())
405
[76]406                if self.VERSION  == 0.8:
[78]407                        tkt = Ticket(self.db, ticket_id)
[77]408                        tkt.save_changes(self.db, self.author, body_text, when)
[76]409                else:
[126]410                        try:
411                                tkt = Ticket(self.env, ticket_id, self.db)
[131]412                        except util.TracError, detail:
[126]413                                return False
414
[77]415                        tkt.save_changes(self.author, body_text, when)
[86]416                        tkt['id'] = ticket_id
[76]417
[129]418                if self.VERSION  == 0.9:
[152]419                        str = self.attachments(m, tkt, True)
[129]420                else:
[152]421                        str = self.attachments(m, tkt)
[76]422
[72]423                if self.notification:
[77]424                        self.notify(tkt, False, when)
[72]425
[71]426                return True
427
[84]428        def new_ticket(self, msg):
[77]429                """
430                Create a new ticket
431                """
[41]432                tkt = Ticket(self.env)
[22]433                tkt['status'] = 'new'
434
435                # Some defaults
436                #
437                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
438                tkt['priority'] = self.get_config('ticket', 'default_priority')
439                tkt['severity'] = self.get_config('ticket', 'default_severity')
440                tkt['version'] = self.get_config('ticket', 'default_version')
441
442                if not msg['Subject']:
[151]443                        tkt['summary'] = u'(No subject)'
[22]444                else:
[139]445                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]446
447
[41]448                if settings.has_key('component'):
[22]449                        tkt['component'] = settings['component']
450                else:
[41]451                        tkt['component'] = self.spam(msg)
[22]452
[118]453                # Discard SPAM messages.
[38]454                #
[122]455                if self.DROP_SPAM and (tkt['component'] == 'Spam'):
[150]456                        if self.DEBUG > 2 :
[149]457                          print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
[124]458                        return False   
[38]459
[43]460                # Set default owner for component
[22]461                #
[43]462                self.set_owner(tkt)
[72]463                self.set_reply_fields(tkt, msg)
[22]464
[45]465                # produce e-mail like header
466                #
[22]467                head = ''
468                if self.EMAIL_HEADER > 0:
469                        head = self.email_header_txt(msg)
[92]470                       
[72]471                body_text = self.get_body_text(msg)
[45]472
[140]473                tkt['description'] = '\r\n%s\r\n%s' \
[142]474                        %(head, body_text)
[90]475
[77]476                when = int(time.time())
[68]477                if self.VERSION == 0.8:
[90]478                        ticket_id = tkt.insert(self.db)
[68]479                else:
[90]480                        ticket_id = tkt.insert()
[98]481                        tkt['id'] = ticket_id
[45]482
[90]483                changed = False
484                comment = ''
[77]485
[90]486                # Rewrite the description if we have mailto enabled
[45]487                #
[72]488                if self.MAILTO:
[100]489                        changed = True
[142]490                        comment = u'\nadded mailto line\n'
491                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
492                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
493                                %(head, mailto, body_text)
[45]494
[152]495                str =  self.attachments(msg, tkt)
496                if str:
[100]497                        changed = True
[152]498                        comment = '%s\n%s\n' %(comment, str)
[77]499
[90]500                if changed:
501                        if self.VERSION  == 0.8:
502                                tkt.save_changes(self.db, self.author, comment)
503                        else:
504                                tkt.save_changes(self.author, comment)
505
506                #print tkt.get_changelog(self.db, when)
507
[45]508                if self.notification:
[90]509                        self.notify(tkt, True)
510                        #self.notify(tkt, False)
[45]511
[77]512        def parse(self, fp):
[96]513                global m
514
[77]515                m = email.message_from_file(fp)
516                if not m:
517                        return
518
519                if self.DEBUG > 1:        # save the entire e-mail message text
520                        self.save_email_for_debug(m)
521                        self.debug_attachments(m)
522
523                self.db = self.env.get_db_cnx()
[149]524                self.get_author_emailaddrs(m)
[152]525
[149]526                if self.blacklisted_from():
527                        if self.DEBUG > 1 :
528                                print 'Message rejected : From: in blacklist'
529                        return False
[77]530
531                if self.get_config('notification', 'smtp_enabled') in ['true']:
532                        self.notification = 1
533                else:
534                        self.notification = 0
535
536                # Must we update existing tickets
537                #
538                if self.TICKET_UPDATE > 0:
539                        if self.ticket_update(m):
540                                return True
541
[84]542                self.new_ticket(m)
[77]543
[136]544        def strip_signature(self, text):
545                """
546                Strip signature from message, inspired by Mailman software
547                """
548                body = []
549                for line in text.splitlines():
550                        if line == '-- ':
551                                break
552                        body.append(line)
553
554                return ('\n'.join(body))
555
[151]556
[154]557        def wrap_text(self, text, replace_whitespace = False):
[151]558                """
559                Will break a lines longer then given length into several small lines of size
560                given length
561                """
562                import textwrap
[154]563
[151]564                LINESEPARATOR = '\n'
[153]565                reformat = ''
[151]566
[154]567                for s in text.split(LINESEPARATOR):
568                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
569                        if tmp:
570                                reformat = '%s\n%s' %(reformat,tmp)
571                        else:
572                                reformat = '%s\n' %reformat
[153]573
574                return reformat
575
[154]576                # Python2.4 and higher
577                #
578                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
579                #
580
581
[72]582        def get_body_text(self, msg):
[45]583                """
[79]584                put the message text in the ticket description or in the changes field.
[45]585                message text can be plain text or html or something else
586                """
[22]587                has_description = 0
[100]588                encoding = True
[109]589                ubody_text = u'No plain text message'
[22]590                for part in msg.walk():
[45]591
592                        # 'multipart/*' is a container for multipart messages
593                        #
594                        if part.get_content_maintype() == 'multipart':
[22]595                                continue
596
597                        if part.get_content_type() == 'text/plain':
[45]598                                # Try to decode, if fails then do not decode
599                                #
[90]600                                body_text = part.get_payload(decode=1)
[45]601                                if not body_text:                       
[90]602                                        body_text = part.get_payload(decode=0)
[154]603       
[136]604                                if self.STRIP_SIGNATURE:
605                                        body_text = self.strip_signature(body_text)
[22]606
[148]607                                if self.USE_TEXTWRAP:
[151]608                                        body_text = self.wrap_text(body_text)
[148]609
[45]610                                # Get contents charset (iso-8859-15 if not defined in mail headers)
611                                #
[100]612                                charset = part.get_content_charset()
[102]613                                if not charset:
614                                        charset = 'iso-8859-15'
615
[89]616                                try:
[96]617                                        ubody_text = unicode(body_text, charset)
[100]618
619                                except UnicodeError, detail:
[96]620                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]621
[100]622                                except LookupError, detail:
[139]623                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]624
[22]625                        elif part.get_content_type() == 'text/html':
[109]626                                ubody_text = '(see attachment for HTML mail message)'
[22]627
628                        else:
[109]629                                ubody_text = '(see attachment for message)'
[22]630
631                        has_description = 1
632                        break           # we have the description, so break
633
634                if not has_description:
[109]635                        ubody_text = '(see attachment for message)'
[22]636
[100]637                # A patch so that the web-interface will not update the description
638                # field of a ticket
639                #
640                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
[22]641
[100]642                #  If we can unicode it try to encode it for trac
643                #  else we a lot of garbage
644                #
[142]645                #if encoding:
646                #       ubody_text = ubody_text.encode('utf-8')
[100]647
[134]648                if self.VERBATIM_FORMAT:
649                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
650                else:
651                        ubody_text = '%s' %ubody_text
652
[100]653                return ubody_text
654
[77]655        def notify(self, tkt , new=True, modtime=0):
[79]656                """
657                A wrapper for the TRAC notify function. So we can use templates
658                """
[112]659                if tkt['component'] == 'Spam':
660                        return 
661
[41]662                try:
663                        # create false {abs_}href properties, to trick Notify()
664                        #
665                        self.env.abs_href = Href(self.get_config('project', 'url'))
666                        self.env.href = Href(self.get_config('project', 'url'))
[22]667
[41]668                        tn = TicketNotifyEmail(self.env)
[42]669                        if self.notify_template:
670                                tn.template_name = self.notify_template;
671
[77]672                        tn.notify(tkt, new, modtime)
[41]673
674                except Exception, e:
[79]675                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
[41]676
[22]677        def mail_line(self, str):
678                return '%s %s' % (self.comment, str)
679
680
[72]681        def html_mailto_link(self, subject, id, body):
682                if not self.author:
[143]683                        author = self.email_addr
[22]684                else:   
[142]685                        author = self.author
[22]686
687                # Must find a fix
688                #
689                #arr = string.split(body, '\n')
690                #arr = map(self.mail_line, arr)
691                #body = string.join(arr, '\n')
692                #body = '%s wrote:\n%s' %(author, body)
693
694                # Temporary fix
[142]695                #
[74]696                str = 'mailto:%s?Subject=%s&Cc=%s' %(
697                       urllib.quote(self.email_addr),
698                           urllib.quote('Re: #%s: %s' %(id, subject)),
699                           urllib.quote(self.MAILTO_CC)
700                           )
701
[90]702                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
[22]703                return str
704
[129]705        def attachments(self, message, ticket, update=False):
[79]706                '''
707                save any attachments as files in the ticket's directory
708                '''
[22]709                count = 0
[77]710                first = 0
711                number = 0
[152]712
713                # Get Maxium attachment size
714                #
715                max_size = int(self.get_config('attachment', 'max_size'))
[153]716                status   = ''
[152]717
[22]718                for part in message.walk():
719                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
720                                continue
721
722                        if not first:                                                                           # first content is the message
723                                first = 1
724                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
725                                        continue
726
727                        filename = part.get_filename()
728                        if not filename:
[77]729                                number = number + 1
730                                filename = 'part%04d' % number
[22]731
[72]732                                ext = mimetypes.guess_extension(part.get_content_type())
[22]733                                if not ext:
734                                        ext = '.bin'
735
736                                filename = '%s%s' % (filename, ext)
737                        else:
[139]738                                filename = self.email_to_unicode(filename)
[22]739
[48]740                        # From the trac code
741                        #
742                        filename = filename.replace('\\', '/').replace(':', '/')
743                        filename = os.path.basename(filename)
[22]744
[48]745                        # We try to normalize the filename to utf-8 NFC if we can.
746                        # Files uploaded from OS X might be in NFD.
[92]747                        # Check python version and then try it
[48]748                        #
749                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
[92]750                                try:
751                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
752                                except TypeError:
753                                        pass
[48]754
[22]755                        url_filename = urllib.quote(filename)
[68]756                        if self.VERSION == 0.8:
[22]757                                dir = os.path.join(self.env.get_attachments_dir(), 'ticket',
758                                                        urllib.quote(str(ticket['id'])))
759                                if not os.path.exists(dir):
760                                        mkdir_p(dir, 0755)
[68]761                        else:
762                                dir = '/tmp'
[22]763
[48]764                        path, fd =  util.create_unique_file(os.path.join(dir, url_filename))
[22]765                        text = part.get_payload(decode=1)
766                        if not text:
767                                text = '(None)'
[48]768                        fd.write(text)
769                        fd.close()
[22]770
[153]771                        # get the file_size
[22]772                        #
[48]773                        stats = os.lstat(path)
[153]774                        file_size = stats[stat.ST_SIZE]
[22]775
[152]776                        # Check if the attachment size is allowed
777                        #
[153]778                        if (max_size != -1) and (file_size > max_size):
779                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
780                                        %(status, filename, file_size, max_size)
[152]781
782                                os.unlink(path)
783                                continue
784                        else:
785                                count = count + 1
786                                       
[22]787                        # Insert the attachment it differs for the different TRAC versions
[73]788                        #
[68]789                        if self.VERSION == 0.8:
[22]790                                cursor = self.db.cursor()
[73]791                                try:
792                                        cursor.execute('INSERT INTO attachment VALUES("%s","%s","%s",%d,%d,"%s","%s","%s")'
[153]793                                                %('ticket', urllib.quote(str(ticket['id'])), filename + '?format=raw', file_size,
[73]794                                                int(time.time()),'', self.author, 'e-mail') )
795
796                                # Attachment is already known
797                                #
798                                except sqlite.IntegrityError:   
[84]799                                        #self.db.close()
800                                        return count
[73]801
[22]802                                self.db.commit()
[73]803
[68]804                        else:
805                                fd = open(path)
806                                att = attachment.Attachment(self.env, 'ticket', ticket['id'])
[129]807
808                                # This will break the ticket_update system, the body_text is vaporized
809                                # ;-(
810                                #
811                                if not update:
812                                        att.author = self.author
[139]813                                        att.description = self.email_to_unicode('Added by email2trac')
[129]814
[153]815                                att.insert(url_filename, fd, file_size)
[150]816
[152]817                                #except  util.TracError, detail:
818                                #       print detail
819
[68]820                                fd.close()
[22]821
[103]822                        # Remove the created temporary filename
823                        #
824                        os.unlink(path)
825
[77]826                # Return how many attachments
827                #
[153]828                status = 'This message has %d attachment(s)\n%s' %(count, status)
829                return status
[22]830
[77]831
[22]832def mkdir_p(dir, mode):
833        '''do a mkdir -p'''
834
835        arr = string.split(dir, '/')
836        path = ''
837        for part in arr:
838                path = '%s/%s' % (path, part)
839                try:
840                        stats = os.stat(path)
841                except OSError:
842                        os.mkdir(path, mode)
843
844
845def ReadConfig(file, name):
846        """
847        Parse the config file
848        """
849
850        if not os.path.isfile(file):
[79]851                print 'File %s does not exist' %file
[22]852                sys.exit(1)
853
854        config = ConfigParser.ConfigParser()
855        try:
856                config.read(file)
857        except ConfigParser.MissingSectionHeaderError,detail:
858                print detail
859                sys.exit(1)
860
861
862        # Use given project name else use defaults
863        #
864        if name:
865                if not config.has_section(name):
[79]866                        print "Not a valid project name: %s" %name
[22]867                        print "Valid names: %s" %config.sections()
868                        sys.exit(1)
869
870                project =  dict()
871                for option in  config.options(name):
872                        project[option] = config.get(name, option)
873
874        else:
875                project = config.defaults()
876
877        return project
878
[87]879
[22]880if __name__ == '__main__':
881        # Default config file
882        #
[24]883        configfile = '@email2trac_conf@'
[22]884        project = ''
885        component = ''
[87]886        ENABLE_SYSLOG = 0
887               
[22]888        try:
889                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
890        except getopt.error,detail:
891                print __doc__
892                print detail
893                sys.exit(1)
[87]894       
[22]895        project_name = None
896        for opt,value in opts:
897                if opt in [ '-h', '--help']:
898                        print __doc__
899                        sys.exit(0)
900                elif opt in ['-c', '--component']:
901                        component = value
902                elif opt in ['-f', '--file']:
903                        configfile = value
904                elif opt in ['-p', '--project']:
905                        project_name = value
[87]906       
[22]907        settings = ReadConfig(configfile, project_name)
908        if not settings.has_key('project'):
909                print __doc__
[79]910                print 'No Trac project is defined in the email2trac config file.'
[22]911                sys.exit(1)
[87]912       
[22]913        if component:
914                settings['component'] = component
[87]915       
[22]916        if settings.has_key('trac_version'):
917                version = float(settings['trac_version'])
918        else:
919                version = trac_default_version
920
[87]921        if settings.has_key('enable_syslog'):
922                ENABLE_SYSLOG =  float(settings['enable_syslog'])
923                       
[22]924        #debug HvB
925        #print settings
[87]926       
927        try:
928                if version == 0.8:
929                        from trac.Environment import Environment
930                        from trac.Ticket import Ticket
931                        from trac.Notify import TicketNotifyEmail
932                        from trac.Href import Href
933                        from trac import util
934                        import sqlite
935                elif version == 0.9:
936                        from trac import attachment
937                        from trac.env import Environment
938                        from trac.ticket import Ticket
939                        from trac.web.href import Href
940                        from trac import util
941                        from trac.Notify import TicketNotifyEmail
942                elif version == 0.10:
943                        from trac import attachment
944                        from trac.env import Environment
945                        from trac.ticket import Ticket
946                        from trac.web.href import Href
947                        from trac import util
[139]948                        #
949                        # return  util.text.to_unicode(str)
950                        #
[87]951                        # see http://projects.edgewall.com/trac/changeset/2799
952                        from trac.ticket.notification import TicketNotifyEmail
953       
954                env = Environment(settings['project'], create=0)
955                tktparser = TicketEmailParser(env, settings, version)
956                tktparser.parse(sys.stdin)
[22]957
[87]958        # Catch all errors ans log to SYSLOG if we have enabled this
959        # else stdout
960        #
961        except Exception, error:
962                if ENABLE_SYSLOG:
963                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
964                        etype, evalue, etb = sys.exc_info()
965                        for e in traceback.format_exception(etype, evalue, etb):
966                                syslog.syslog(e)
967                        syslog.closelog()
968                else:
969                        traceback.print_exc()
[22]970
[97]971                if m:
[98]972                        tktparser.save_email_for_debug(m, True)
[97]973
[22]974# EOB
Note: See TracBrowser for help on using the repository browser.