source: trunk/email2trac.py.in @ 167

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

email2trac.py.in:

  • Renamed some functions and added some new for ticket field manipulation with mail
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 25.4 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 167 2007-07-05 17:00:55Z 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
[167]380        def str_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
[167]398
399        def update_ticket_fields(self, system_fields, user_fields):
400                for field in system_fields:
401                        print field['name']
402                        try:
403                                print field['options']
404                        except KeyError:
405                                print field
406                                pass
[164]407                               
[71]408        def ticket_update(self, m):
[78]409                """
[79]410                If the current email is a reply to an existing ticket, this function
411                will append the contents of this email to that ticket, instead of
412                creating a new one.
[78]413                """
[71]414                if not m['Subject']:
415                        return False
416                else:
[139]417                        subject  = self.email_to_unicode(m['Subject'])
[71]418
419                TICKET_RE = re.compile(r"""
420                                        (?P<ticketnr>[#][0-9]+:)
[164]421                                        |(?P<ticketnr_fields>[#][\d]+\?.*:)
[71]422                                        """, re.VERBOSE)
423
424                result =  TICKET_RE.search(subject)
425                if not result:
426                        return False
427
[78]428                body_text = self.get_body_text(m)
429
[164]430                # Must we update ticket fields
431                #
[167]432                update_tkt_fields = dict()
[165]433                try:
[164]434                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
[167]435                        update_tkt_fields = self.str_to_dict(keywords)
[165]436
437                        # Strip '#'
438                        #
439                        ticket_id = int(nr[1:])
440
441                except AttributeError:
442                        # Strip '#' and ':'
443                        #
[167]444                        nr = result.group('ticketnr')
[165]445                        ticket_id = int(nr[1:-1])
[164]446
[71]447
[77]448                # Get current time
449                #
450                when = int(time.time())
451
[76]452                if self.VERSION  == 0.8:
[78]453                        tkt = Ticket(self.db, ticket_id)
[77]454                        tkt.save_changes(self.db, self.author, body_text, when)
[76]455                else:
[126]456                        try:
457                                tkt = Ticket(self.env, ticket_id, self.db)
[131]458                        except util.TracError, detail:
[126]459                                return False
460
[165]461                        # Must we update some ticket fields
462                        #
[167]463                        self.update_ticket_fields(tkt.fields, update_tkt_fields)
464                        sys.exit(1)
[166]465
[165]466                        for key,value in tkt_fields.items():
467                                tkt[key] = value
468
[77]469                        tkt.save_changes(self.author, body_text, when)
[86]470                        tkt['id'] = ticket_id
[76]471
[129]472                if self.VERSION  == 0.9:
[152]473                        str = self.attachments(m, tkt, True)
[129]474                else:
[152]475                        str = self.attachments(m, tkt)
[76]476
[72]477                if self.notification:
[77]478                        self.notify(tkt, False, when)
[72]479
[71]480                return True
481
[84]482        def new_ticket(self, msg):
[77]483                """
484                Create a new ticket
485                """
[41]486                tkt = Ticket(self.env)
[22]487                tkt['status'] = 'new'
488
489                # Some defaults
490                #
491                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
492                tkt['priority'] = self.get_config('ticket', 'default_priority')
493                tkt['severity'] = self.get_config('ticket', 'default_severity')
494                tkt['version'] = self.get_config('ticket', 'default_version')
495
496                if not msg['Subject']:
[151]497                        tkt['summary'] = u'(No subject)'
[22]498                else:
[139]499                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]500
501
[41]502                if settings.has_key('component'):
[22]503                        tkt['component'] = settings['component']
504                else:
[41]505                        tkt['component'] = self.spam(msg)
[22]506
[118]507                # Discard SPAM messages.
[38]508                #
[122]509                if self.DROP_SPAM and (tkt['component'] == 'Spam'):
[150]510                        if self.DEBUG > 2 :
[149]511                          print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
[124]512                        return False   
[38]513
[43]514                # Set default owner for component
[22]515                #
[43]516                self.set_owner(tkt)
[72]517                self.set_reply_fields(tkt, msg)
[22]518
[45]519                # produce e-mail like header
520                #
[22]521                head = ''
522                if self.EMAIL_HEADER > 0:
523                        head = self.email_header_txt(msg)
[92]524                       
[72]525                body_text = self.get_body_text(msg)
[45]526
[140]527                tkt['description'] = '\r\n%s\r\n%s' \
[142]528                        %(head, body_text)
[90]529
[77]530                when = int(time.time())
[68]531                if self.VERSION == 0.8:
[90]532                        ticket_id = tkt.insert(self.db)
[68]533                else:
[90]534                        ticket_id = tkt.insert()
[98]535                        tkt['id'] = ticket_id
[45]536
[90]537                changed = False
538                comment = ''
[77]539
[90]540                # Rewrite the description if we have mailto enabled
[45]541                #
[72]542                if self.MAILTO:
[100]543                        changed = True
[142]544                        comment = u'\nadded mailto line\n'
545                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
546                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
547                                %(head, mailto, body_text)
[45]548
[152]549                str =  self.attachments(msg, tkt)
550                if str:
[100]551                        changed = True
[152]552                        comment = '%s\n%s\n' %(comment, str)
[77]553
[90]554                if changed:
555                        if self.VERSION  == 0.8:
556                                tkt.save_changes(self.db, self.author, comment)
557                        else:
558                                tkt.save_changes(self.author, comment)
559
560                #print tkt.get_changelog(self.db, when)
561
[45]562                if self.notification:
[90]563                        self.notify(tkt, True)
564                        #self.notify(tkt, False)
[45]565
[77]566        def parse(self, fp):
[96]567                global m
568
[77]569                m = email.message_from_file(fp)
570                if not m:
571                        return
572
573                if self.DEBUG > 1:        # save the entire e-mail message text
574                        self.save_email_for_debug(m)
575                        self.debug_attachments(m)
576
577                self.db = self.env.get_db_cnx()
[149]578                self.get_author_emailaddrs(m)
[152]579
[149]580                if self.blacklisted_from():
581                        if self.DEBUG > 1 :
582                                print 'Message rejected : From: in blacklist'
583                        return False
[77]584
585                if self.get_config('notification', 'smtp_enabled') in ['true']:
586                        self.notification = 1
587                else:
588                        self.notification = 0
589
590                # Must we update existing tickets
591                #
592                if self.TICKET_UPDATE > 0:
593                        if self.ticket_update(m):
594                                return True
595
[84]596                self.new_ticket(m)
[77]597
[136]598        def strip_signature(self, text):
599                """
600                Strip signature from message, inspired by Mailman software
601                """
602                body = []
603                for line in text.splitlines():
604                        if line == '-- ':
605                                break
606                        body.append(line)
607
608                return ('\n'.join(body))
609
[151]610
[154]611        def wrap_text(self, text, replace_whitespace = False):
[151]612                """
613                Will break a lines longer then given length into several small lines of size
614                given length
615                """
616                import textwrap
[154]617
[151]618                LINESEPARATOR = '\n'
[153]619                reformat = ''
[151]620
[154]621                for s in text.split(LINESEPARATOR):
622                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
623                        if tmp:
624                                reformat = '%s\n%s' %(reformat,tmp)
625                        else:
626                                reformat = '%s\n' %reformat
[153]627
628                return reformat
629
[154]630                # Python2.4 and higher
631                #
632                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
633                #
634
635
[72]636        def get_body_text(self, msg):
[45]637                """
[79]638                put the message text in the ticket description or in the changes field.
[45]639                message text can be plain text or html or something else
640                """
[22]641                has_description = 0
[100]642                encoding = True
[109]643                ubody_text = u'No plain text message'
[22]644                for part in msg.walk():
[45]645
646                        # 'multipart/*' is a container for multipart messages
647                        #
648                        if part.get_content_maintype() == 'multipart':
[22]649                                continue
650
651                        if part.get_content_type() == 'text/plain':
[45]652                                # Try to decode, if fails then do not decode
653                                #
[90]654                                body_text = part.get_payload(decode=1)
[45]655                                if not body_text:                       
[90]656                                        body_text = part.get_payload(decode=0)
[154]657       
[136]658                                if self.STRIP_SIGNATURE:
659                                        body_text = self.strip_signature(body_text)
[22]660
[148]661                                if self.USE_TEXTWRAP:
[151]662                                        body_text = self.wrap_text(body_text)
[148]663
[45]664                                # Get contents charset (iso-8859-15 if not defined in mail headers)
665                                #
[100]666                                charset = part.get_content_charset()
[102]667                                if not charset:
668                                        charset = 'iso-8859-15'
669
[89]670                                try:
[96]671                                        ubody_text = unicode(body_text, charset)
[100]672
673                                except UnicodeError, detail:
[96]674                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]675
[100]676                                except LookupError, detail:
[139]677                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]678
[22]679                        elif part.get_content_type() == 'text/html':
[109]680                                ubody_text = '(see attachment for HTML mail message)'
[22]681
682                        else:
[109]683                                ubody_text = '(see attachment for message)'
[22]684
685                        has_description = 1
686                        break           # we have the description, so break
687
688                if not has_description:
[109]689                        ubody_text = '(see attachment for message)'
[22]690
[100]691                # A patch so that the web-interface will not update the description
692                # field of a ticket
693                #
694                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
[22]695
[100]696                #  If we can unicode it try to encode it for trac
697                #  else we a lot of garbage
698                #
[142]699                #if encoding:
700                #       ubody_text = ubody_text.encode('utf-8')
[100]701
[134]702                if self.VERBATIM_FORMAT:
703                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
704                else:
705                        ubody_text = '%s' %ubody_text
706
[100]707                return ubody_text
708
[77]709        def notify(self, tkt , new=True, modtime=0):
[79]710                """
711                A wrapper for the TRAC notify function. So we can use templates
712                """
[112]713                if tkt['component'] == 'Spam':
714                        return 
715
[41]716                try:
717                        # create false {abs_}href properties, to trick Notify()
718                        #
719                        self.env.abs_href = Href(self.get_config('project', 'url'))
720                        self.env.href = Href(self.get_config('project', 'url'))
[22]721
[41]722                        tn = TicketNotifyEmail(self.env)
[42]723                        if self.notify_template:
724                                tn.template_name = self.notify_template;
725
[77]726                        tn.notify(tkt, new, modtime)
[41]727
728                except Exception, e:
[79]729                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
[41]730
[22]731        def mail_line(self, str):
732                return '%s %s' % (self.comment, str)
733
734
[72]735        def html_mailto_link(self, subject, id, body):
736                if not self.author:
[143]737                        author = self.email_addr
[22]738                else:   
[142]739                        author = self.author
[22]740
741                # Must find a fix
742                #
743                #arr = string.split(body, '\n')
744                #arr = map(self.mail_line, arr)
745                #body = string.join(arr, '\n')
746                #body = '%s wrote:\n%s' %(author, body)
747
748                # Temporary fix
[142]749                #
[74]750                str = 'mailto:%s?Subject=%s&Cc=%s' %(
751                       urllib.quote(self.email_addr),
752                           urllib.quote('Re: #%s: %s' %(id, subject)),
753                           urllib.quote(self.MAILTO_CC)
754                           )
755
[90]756                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
[22]757                return str
758
[129]759        def attachments(self, message, ticket, update=False):
[79]760                '''
761                save any attachments as files in the ticket's directory
762                '''
[22]763                count = 0
[77]764                first = 0
765                number = 0
[152]766
767                # Get Maxium attachment size
768                #
769                max_size = int(self.get_config('attachment', 'max_size'))
[153]770                status   = ''
[152]771
[22]772                for part in message.walk():
773                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
774                                continue
775
776                        if not first:                                                                           # first content is the message
777                                first = 1
778                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
779                                        continue
780
781                        filename = part.get_filename()
782                        if not filename:
[77]783                                number = number + 1
784                                filename = 'part%04d' % number
[22]785
[72]786                                ext = mimetypes.guess_extension(part.get_content_type())
[22]787                                if not ext:
788                                        ext = '.bin'
789
790                                filename = '%s%s' % (filename, ext)
791                        else:
[139]792                                filename = self.email_to_unicode(filename)
[22]793
[48]794                        # From the trac code
795                        #
796                        filename = filename.replace('\\', '/').replace(':', '/')
797                        filename = os.path.basename(filename)
[22]798
[48]799                        # We try to normalize the filename to utf-8 NFC if we can.
800                        # Files uploaded from OS X might be in NFD.
[92]801                        # Check python version and then try it
[48]802                        #
803                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
[92]804                                try:
805                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
806                                except TypeError:
807                                        pass
[48]808
[22]809                        url_filename = urllib.quote(filename)
[68]810                        if self.VERSION == 0.8:
[22]811                                dir = os.path.join(self.env.get_attachments_dir(), 'ticket',
812                                                        urllib.quote(str(ticket['id'])))
813                                if not os.path.exists(dir):
814                                        mkdir_p(dir, 0755)
[68]815                        else:
816                                dir = '/tmp'
[22]817
[48]818                        path, fd =  util.create_unique_file(os.path.join(dir, url_filename))
[22]819                        text = part.get_payload(decode=1)
820                        if not text:
821                                text = '(None)'
[48]822                        fd.write(text)
823                        fd.close()
[22]824
[153]825                        # get the file_size
[22]826                        #
[48]827                        stats = os.lstat(path)
[153]828                        file_size = stats[stat.ST_SIZE]
[22]829
[152]830                        # Check if the attachment size is allowed
831                        #
[153]832                        if (max_size != -1) and (file_size > max_size):
833                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
834                                        %(status, filename, file_size, max_size)
[152]835
836                                os.unlink(path)
837                                continue
838                        else:
839                                count = count + 1
840                                       
[22]841                        # Insert the attachment it differs for the different TRAC versions
[73]842                        #
[68]843                        if self.VERSION == 0.8:
[22]844                                cursor = self.db.cursor()
[73]845                                try:
846                                        cursor.execute('INSERT INTO attachment VALUES("%s","%s","%s",%d,%d,"%s","%s","%s")'
[153]847                                                %('ticket', urllib.quote(str(ticket['id'])), filename + '?format=raw', file_size,
[73]848                                                int(time.time()),'', self.author, 'e-mail') )
849
850                                # Attachment is already known
851                                #
852                                except sqlite.IntegrityError:   
[84]853                                        #self.db.close()
854                                        return count
[73]855
[22]856                                self.db.commit()
[73]857
[68]858                        else:
859                                fd = open(path)
860                                att = attachment.Attachment(self.env, 'ticket', ticket['id'])
[129]861
862                                # This will break the ticket_update system, the body_text is vaporized
863                                # ;-(
864                                #
865                                if not update:
866                                        att.author = self.author
[139]867                                        att.description = self.email_to_unicode('Added by email2trac')
[129]868
[153]869                                att.insert(url_filename, fd, file_size)
[150]870
[152]871                                #except  util.TracError, detail:
872                                #       print detail
873
[68]874                                fd.close()
[22]875
[103]876                        # Remove the created temporary filename
877                        #
878                        os.unlink(path)
879
[77]880                # Return how many attachments
881                #
[153]882                status = 'This message has %d attachment(s)\n%s' %(count, status)
883                return status
[22]884
[77]885
[22]886def mkdir_p(dir, mode):
887        '''do a mkdir -p'''
888
889        arr = string.split(dir, '/')
890        path = ''
891        for part in arr:
892                path = '%s/%s' % (path, part)
893                try:
894                        stats = os.stat(path)
895                except OSError:
896                        os.mkdir(path, mode)
897
898
899def ReadConfig(file, name):
900        """
901        Parse the config file
902        """
903
904        if not os.path.isfile(file):
[79]905                print 'File %s does not exist' %file
[22]906                sys.exit(1)
907
908        config = ConfigParser.ConfigParser()
909        try:
910                config.read(file)
911        except ConfigParser.MissingSectionHeaderError,detail:
912                print detail
913                sys.exit(1)
914
915
916        # Use given project name else use defaults
917        #
918        if name:
919                if not config.has_section(name):
[79]920                        print "Not a valid project name: %s" %name
[22]921                        print "Valid names: %s" %config.sections()
922                        sys.exit(1)
923
924                project =  dict()
925                for option in  config.options(name):
926                        project[option] = config.get(name, option)
927
928        else:
929                project = config.defaults()
930
931        return project
932
[87]933
[22]934if __name__ == '__main__':
935        # Default config file
936        #
[24]937        configfile = '@email2trac_conf@'
[22]938        project = ''
939        component = ''
[87]940        ENABLE_SYSLOG = 0
941               
[22]942        try:
943                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
944        except getopt.error,detail:
945                print __doc__
946                print detail
947                sys.exit(1)
[87]948       
[22]949        project_name = None
950        for opt,value in opts:
951                if opt in [ '-h', '--help']:
952                        print __doc__
953                        sys.exit(0)
954                elif opt in ['-c', '--component']:
955                        component = value
956                elif opt in ['-f', '--file']:
957                        configfile = value
958                elif opt in ['-p', '--project']:
959                        project_name = value
[87]960       
[22]961        settings = ReadConfig(configfile, project_name)
962        if not settings.has_key('project'):
963                print __doc__
[79]964                print 'No Trac project is defined in the email2trac config file.'
[22]965                sys.exit(1)
[87]966       
[22]967        if component:
968                settings['component'] = component
[87]969       
[22]970        if settings.has_key('trac_version'):
971                version = float(settings['trac_version'])
972        else:
973                version = trac_default_version
974
[87]975        if settings.has_key('enable_syslog'):
976                ENABLE_SYSLOG =  float(settings['enable_syslog'])
977                       
[22]978        #debug HvB
979        #print settings
[87]980       
981        try:
982                if version == 0.8:
983                        from trac.Environment import Environment
984                        from trac.Ticket import Ticket
985                        from trac.Notify import TicketNotifyEmail
986                        from trac.Href import Href
987                        from trac import util
988                        import sqlite
989                elif version == 0.9:
990                        from trac import attachment
991                        from trac.env import Environment
992                        from trac.ticket import Ticket
993                        from trac.web.href import Href
994                        from trac import util
995                        from trac.Notify import TicketNotifyEmail
996                elif version == 0.10:
997                        from trac import attachment
998                        from trac.env import Environment
999                        from trac.ticket import Ticket
1000                        from trac.web.href import Href
1001                        from trac import util
[139]1002                        #
1003                        # return  util.text.to_unicode(str)
1004                        #
[87]1005                        # see http://projects.edgewall.com/trac/changeset/2799
1006                        from trac.ticket.notification import TicketNotifyEmail
1007       
1008                env = Environment(settings['project'], create=0)
1009                tktparser = TicketEmailParser(env, settings, version)
1010                tktparser.parse(sys.stdin)
[22]1011
[87]1012        # Catch all errors ans log to SYSLOG if we have enabled this
1013        # else stdout
1014        #
1015        except Exception, error:
1016                if ENABLE_SYSLOG:
1017                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1018                        etype, evalue, etb = sys.exc_info()
1019                        for e in traceback.format_exception(etype, evalue, etb):
1020                                syslog.syslog(e)
1021                        syslog.closelog()
1022                else:
1023                        traceback.print_exc()
[22]1024
[97]1025                if m:
[98]1026                        tktparser.save_email_for_debug(m, True)
[97]1027
[22]1028# EOB
Note: See TracBrowser for help on using the repository browser.