source: trunk/email2trac.py.in @ 176

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

email2trac.py.in, ChangeLog?:

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