source: trunk/email2trac.py.in @ 166

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

email2trac.py.in:

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