source: trunk/email2trac.py.in @ 164

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

email2trac.py.in:

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