source: trunk/email2trac.py.in @ 173

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

email2trac.py.in, email2trac.conf:

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