source: trunk/email2trac.py.in @ 165

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

email2trac.py.in:

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