source: trunk/email2trac.py.in @ 253

Last change on this file since 253 was 253, checked in by bas, 15 years ago

email2trac.py.in:

  • ticket id is now move to the class variable. Adjusted the functions to new situation
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 37.9 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:
[205]68                -h,--help
69                -f,--file  <configuration file>
70                -n,--dry-run
71                -p, --project <project name>
72                -t, --ticket_prefix <name>
[22]73
74SVN Info:
75        $Id: email2trac.py.in 253 2009-01-28 08:25:08Z bas $
76"""
77import os
78import sys
79import string
80import getopt
81import stat
82import time
83import email
[136]84import email.Iterators
85import email.Header
[22]86import re
87import urllib
88import unicodedata
89from stat import *
90import mimetypes
[96]91import traceback
[22]92
[190]93
94# Will fail where unavailable, e.g. Windows
95#
96try:
97    import syslog
98    SYSLOG_AVAILABLE = True
99except ImportError:
100    SYSLOG_AVAILABLE = False
101
[182]102from datetime import tzinfo, timedelta, datetime
[199]103from trac import config as trac_config
[91]104
[96]105# Some global variables
106#
[189]107trac_default_version = '0.10'
[96]108m = None
[22]109
[182]110# A UTC class needed for trac version 0.11, added by
111# tbaschak at ktc dot mb dot ca
112#
113class UTC(tzinfo):
114        """UTC"""
115        ZERO = timedelta(0)
116        HOUR = timedelta(hours=1)
117       
118        def utcoffset(self, dt):
119                return self.ZERO
120               
121        def tzname(self, dt):
122                return "UTC"
123               
124        def dst(self, dt):
125                return self.ZERO
126
127
[22]128class TicketEmailParser(object):
129        env = None
130        comment = '> '
131   
[206]132        def __init__(self, env, parameters, version):
[22]133                self.env = env
134
135                # Database connection
136                #
137                self.db = None
138
[206]139                # Save parameters
140                #
141                self.parameters = parameters
142
[72]143                # Some useful mail constants
144                #
145                self.author = None
146                self.email_addr = None
[183]147                self.email_from = None
[253]148                self.id = None
[72]149
[22]150                self.VERSION = version
[206]151                self.DRY_RUN = parameters['dry_run']
[204]152
[172]153                self.get_config = self.env.config.get
[22]154
155                if parameters.has_key('umask'):
156                        os.umask(int(parameters['umask'], 8))
157
[236]158                if parameters.has_key('quote_attachment_filenames'):
159                        self.QUOTE_ATTACHMENT_FILENAMES = int(parameters['quote_attachment_filenames'])
160                else:
161                        self.QUOTE_ATTACHMENT_FILENAMES = 1
162
[22]163                if parameters.has_key('debug'):
164                        self.DEBUG = int(parameters['debug'])
165                else:
166                        self.DEBUG = 0
167
168                if parameters.has_key('mailto_link'):
169                        self.MAILTO = int(parameters['mailto_link'])
[74]170                        if parameters.has_key('mailto_cc'):
171                                self.MAILTO_CC = parameters['mailto_cc']
172                        else:
173                                self.MAILTO_CC = ''
[22]174                else:
175                        self.MAILTO = 0
176
177                if parameters.has_key('spam_level'):
178                        self.SPAM_LEVEL = int(parameters['spam_level'])
179                else:
180                        self.SPAM_LEVEL = 0
181
[207]182                if parameters.has_key('spam_header'):
183                        self.SPAM_HEADER = parameters['spam_header']
184                else:
185                        self.SPAM_HEADER = 'X-Spam-Score'
186
[191]187                if parameters.has_key('email_quote'):
188                        self.EMAIL_QUOTE = str(parameters['email_quote'])
189                else:   
190                        self.EMAIL_QUOTE = '> '
[22]191
192                if parameters.has_key('email_header'):
193                        self.EMAIL_HEADER = int(parameters['email_header'])
194                else:
195                        self.EMAIL_HEADER = 0
196
[42]197                if parameters.has_key('alternate_notify_template'):
198                        self.notify_template = str(parameters['alternate_notify_template'])
199                else:
200                        self.notify_template = None
[22]201
[222]202                if parameters.has_key('alternate_notify_template_update'):
203                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
204                else:
205                        self.notify_template_update = None
206
[43]207                if parameters.has_key('reply_all'):
208                        self.REPLY_ALL = int(parameters['reply_all'])
209                else:
210                        self.REPLY_ALL = 0
[42]211
[74]212                if parameters.has_key('ticket_update'):
213                        self.TICKET_UPDATE = int(parameters['ticket_update'])
214                else:
215                        self.TICKET_UPDATE = 0
[43]216
[118]217                if parameters.has_key('drop_spam'):
218                        self.DROP_SPAM = int(parameters['drop_spam'])
219                else:
220                        self.DROP_SPAM = 0
[74]221
[134]222                if parameters.has_key('verbatim_format'):
223                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
224                else:
225                        self.VERBATIM_FORMAT = 1
[118]226
[231]227                if parameters.has_key('reflow'):
228                        self.REFLOW = int(parameter['reflow'])
229                else:
230                        self.REFLOW = 1
231
[136]232                if parameters.has_key('strip_signature'):
233                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
234                else:
235                        self.STRIP_SIGNATURE = 0
[134]236
[191]237                if parameters.has_key('strip_quotes'):
238                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
239                else:
240                        self.STRIP_QUOTES = 0
241
[148]242                if parameters.has_key('use_textwrap'):
243                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
244                else:
245                        self.USE_TEXTWRAP = 0
246
[238]247                if parameters.has_key('binhex'):
248                        self.BINHEX = parameters['binhex']
249                else:
250                        self.BINHEX = 'warn'
251
252                if parameters.has_key('applesingle'):
253                        self.APPLESINGLE = parameters['applesingle']
254                else:
255                        self.APPLESINGLE = 'warn'
256
257                if parameters.has_key('appledouble'):
258                        self.APPLEDOUBLE = parameters['appledouble']
259                else:
260                        self.APPLEDOUBLE = 'warn'
261
[163]262                if parameters.has_key('python_egg_cache'):
263                        self.python_egg_cache = str(parameters['python_egg_cache'])
264                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
265
[173]266                # Use OS independend functions
267                #
268                self.TMPDIR = os.path.normcase('/tmp')
269                if parameters.has_key('tmpdir'):
270                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
271
[194]272                if parameters.has_key('ignore_trac_user_settings'):
273                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
274                else:
275                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]276
[22]277        def spam(self, message):
[191]278                """
279                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
280                # Note if Spam_level then '*' are included
281                """
[194]282                spam = False
[207]283                if message.has_key(self.SPAM_HEADER):
284                        spam_l = string.split(message[self.SPAM_HEADER])
[22]285
[207]286                        try:
287                                number = spam_l[0].count('*')
288                        except IndexError, detail:
289                                number = 0
290                               
[22]291                        if number >= self.SPAM_LEVEL:
[194]292                                spam = True
293                               
[191]294                # treat virus mails as spam
295                #
296                elif message.has_key('X-Virus-found'):                 
[194]297                        spam = True
298
299                # How to handle SPAM messages
300                #
301                if self.DROP_SPAM and spam:
302                        if self.DEBUG > 2 :
303                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
304
[204]305                        return 'drop'   
[194]306
307                elif spam:
308
[204]309                        return 'Spam'   
[67]310
[194]311                else:
[22]312
[204]313                        return False
[191]314
[221]315        def email_header_acl(self, keyword, header_field, default):
[206]316                """
[221]317                This function wil check if the email address is allowed or denied
318                to send mail to the ticket list
319            """
[206]320                try:
[221]321                        mail_addresses = self.parameters[keyword]
322
323                        # Check if we have an empty string
324                        #
325                        if not mail_addresses:
326                                return default
327
[206]328                except KeyError, detail:
[221]329                        if self.DEBUG > 2 :
[250]330                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
[206]331
[221]332                        return default
[206]333
[221]334                mail_addresses = string.split(mail_addresses, ',')
335
336                for entry in mail_addresses:
[209]337                        entry = entry.strip()
[221]338                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
339                        result =  TO_RE.search(header_field)
[208]340                        if result:
341                                return True
[149]342
[208]343                return False
344
[139]345        def email_to_unicode(self, message_str):
[22]346                """
347                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]348        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]349                understands it.
350                """
[139]351                results =  email.Header.decode_header(message_str)
[22]352                str = None
353                for text,format in results:
354                        if format:
355                                try:
356                                        temp = unicode(text, format)
[139]357                                except UnicodeError, detail:
[22]358                                        # This always works
359                                        #
360                                        temp = unicode(text, 'iso-8859-15')
[139]361                                except LookupError, detail:
362                                        #text = 'ERROR: Could not find charset: %s, please install' %format
363                                        #temp = unicode(text, 'iso-8859-15')
364                                        temp = message_str
365                                       
[22]366                        else:
367                                temp = string.strip(text)
[92]368                                temp = unicode(text, 'iso-8859-15')
[22]369
370                        if str:
[100]371                                str = '%s %s' %(str, temp)
[22]372                        else:
[100]373                                str = '%s' %temp
[22]374
[139]375                #str = str.encode('utf-8')
[22]376                return str
377
[236]378        def debug_body(self, message_body, tempfile=False):
379                if tempfile:
380                        import tempfile
381                        body_file = tempfile.mktemp('.email2trac')
382                else:
383                        body_file = os.path.join(self.TMPDIR, 'body.txt')
384
385                print 'TD: writing body (%s)' % body_file
386                fx = open(body_file, 'wb')
387                if not message_body:
388                        message_body = '(None)'
389                fx.write(message_body)
390                fx.close()
391                try:
392                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
393                except OSError:
394                        pass
395
396        def debug_attachments(self, message_parts):
[22]397                n = 0
[236]398                for part in message_parts:
399                        # Skip inline text parts
400                        if not isinstance(part, tuple):
[22]401                                continue
[236]402                               
[237]403                        (original, filename, part) = part
[22]404
405                        n = n + 1
406                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
407                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
408
[236]409                        part_file = os.path.join(self.TMPDIR, filename)
[173]410                        #part_file = '/var/tmp/part%d' % n
[22]411                        print 'TD: writing part%d (%s)' % (n,part_file)
412                        fx = open(part_file, 'wb')
413                        text = part.get_payload(decode=1)
414                        if not text:
415                                text = '(None)'
416                        fx.write(text)
417                        fx.close()
418                        try:
419                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
420                        except OSError:
421                                pass
422
423        def email_header_txt(self, m):
[72]424                """
425                Display To and CC addresses in description field
426                """
[22]427                str = ''
[213]428                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
429                if m['To'] and len(m['To']) > 0:
430                        str = "'''To:''' %s\r\n" %(m['To'])
[22]431                if m['Cc'] and len(m['Cc']) > 0:
[213]432                        str = "%s'''Cc:''' %s\r\n" % (str, m['Cc'])
[22]433
[139]434                return  self.email_to_unicode(str)
[22]435
[138]436
[194]437        def get_sender_info(self, message):
[45]438                """
[72]439                Get the default author name and email address from the message
[226]440                """
[43]441
[226]442                self.email_to = self.email_to_unicode(message['to'])
443                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
444
[194]445                self.email_from = self.email_to_unicode(message['from'])
[223]446                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]447
[252]448                # Trac can not handle author's name that contains spaces
449                #
450                self.author = self.email_addr
[194]451
452                if self.IGNORE_TRAC_USER_SETTINGS:
453                        return
454
455                # Is this a registered user, use email address as search key:
456                # result:
457                #   u : login name
458                #   n : Name that the user has set in the settings tab
459                #   e : email address that the user has set in the settings tab
[45]460                #
[194]461                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]462                        if e and (e.lower() == self.email_addr.lower()) ]
[43]463
[45]464                if len(users) == 1:
[194]465                        self.email_from = users[0][0]
[250]466                        self.author = users[0][0]
[45]467
[72]468        def set_reply_fields(self, ticket, message):
469                """
470                Set all the right fields for a new ticket
471                """
[183]472                ticket['reporter'] = self.email_from
[72]473
[45]474                # Put all CC-addresses in ticket CC field
[43]475                #
476                if self.REPLY_ALL:
[45]477                        #tos = message.get_all('to', [])
[43]478                        ccs = message.get_all('cc', [])
479
[45]480                        addrs = email.Utils.getaddresses(ccs)
[105]481                        if not addrs:
482                                return
[43]483
484                        # Remove reporter email address if notification is
485                        # on
486                        #
487                        if self.notification:
488                                try:
[72]489                                        addrs.remove((self.author, self.email_addr))
[43]490                                except ValueError, detail:
491                                        pass
492
[45]493                        for name,mail in addrs:
[105]494                                try:
[108]495                                        mail_list = '%s, %s' %(mail_list, mail)
[230]496                                except UnboundLocalError, detail:
[105]497                                        mail_list = mail
[43]498
[105]499                        if mail_list:
[139]500                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]501
502        def save_email_for_debug(self, message, tempfile=False):
503                if tempfile:
504                        import tempfile
505                        msg_file = tempfile.mktemp('.email2trac')
506                else:
[173]507                        #msg_file = '/var/tmp/msg.txt'
508                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
509
[44]510                print 'TD: saving email to %s' % msg_file
511                fx = open(msg_file, 'wb')
512                fx.write('%s' % message)
513                fx.close()
514                try:
515                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
516                except OSError:
517                        pass
518
[167]519        def str_to_dict(self, str):
[164]520                """
521                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
522                """
523                # Skip the last ':' character
524                #
525                fields = string.split(str[:-1], ',')
526
527                result = dict()
528                for field in fields:
529                        try:
530                                index, value = string.split(field,'=')
[169]531
532                                # We can not change the description of a ticket via the subject
533                                # line. The description is the body of the email
534                                #
535                                if index.lower() in ['description']:
536                                        continue
537
[164]538                                if value:
[165]539                                        result[index.lower()] = value
[169]540
[164]541                        except ValueError:
542                                pass
543
[165]544                return result
[167]545
[202]546        def update_ticket_fields(self, ticket, user_dict, use_default=None):
547                """
548                This will update the ticket fields. It will check if the
549                given fields are known and if the right values are specified
550                It will only update the ticket field value:
[169]551                        - If the field is known
[202]552                        - If the value supplied is valid for the ticket field.
553                          If not then there are two options:
554                           1) Skip the value (use_default=None)
555                           2) Set default value for field (use_default=1)
[169]556                """
557
558                # Build a system dictionary from the ticket fields
559                # with field as index and option as value
560                #
561                sys_dict = dict()
562                for field in ticket.fields:
[167]563                        try:
[169]564                                sys_dict[field['name']] = field['options']
565
[167]566                        except KeyError:
[169]567                                sys_dict[field['name']] = None
[167]568                                pass
[169]569
570                # Check user supplied fields an compare them with the
571                # system one's
572                #
573                for field,value in user_dict.items():
[202]574                        if self.DEBUG >= 10:
575                                print  'user_field\t %s = %s' %(field,value)
[169]576
577                        if sys_dict.has_key(field):
578
579                                # Check if value is an allowed system option, if TypeError then
580                                # every value is allowed
581                                #
582                                try:
583                                        if value in sys_dict[field]:
584                                                ticket[field] = value
[202]585                                        else:
586                                                # Must we set a default if value is not allowed
587                                                #
588                                                if use_default:
589                                                        value = self.get_config('ticket', 'default_%s' %(field) )
590                                                        ticket[field] = value
[169]591
592                                except TypeError:
593                                        ticket[field] = value
[202]594
595                                if self.DEBUG >= 10:
596                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]597                                       
[204]598        def ticket_update(self, m, spam):
[78]599                """
[79]600                If the current email is a reply to an existing ticket, this function
601                will append the contents of this email to that ticket, instead of
602                creating a new one.
[78]603                """
[250]604                if self.DEBUG:
605                        print "TD: ticket_update"
[202]606
[71]607                if not m['Subject']:
608                        return False
609                else:
[139]610                        subject  = self.email_to_unicode(m['Subject'])
[71]611
[182]612                # [hic] #1529: Re: LRZ
613                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
614                #
[71]615                TICKET_RE = re.compile(r"""
616                                        (?P<ticketnr>[#][0-9]+:)
[194]617                                        |(?P<ticketnr_fields>[#][\d]+\?.*?:)
[71]618                                        """, re.VERBOSE)
619
620                result =  TICKET_RE.search(subject)
621                if not result:
622                        return False
623
[164]624                # Must we update ticket fields
625                #
[220]626                update_fields = dict()
[165]627                try:
[164]628                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
[220]629                        update_fields = self.str_to_dict(keywords)
[165]630
631                        # Strip '#'
632                        #
[253]633                        self.id = int(nr[1:])
[165]634
635                except AttributeError:
636                        # Strip '#' and ':'
637                        #
[167]638                        nr = result.group('ticketnr')
[253]639                        self.id = int(nr[1:-1])
[164]640
[71]641
[194]642                # When is the change committed
643                #
[77]644                #
[194]645                if self.VERSION == 0.11:
646                        utc = UTC()
647                        when = datetime.now(utc)
648                else:
649                        when = int(time.time())
[77]650
[172]651                try:
[253]652                        tkt = Ticket(self.env, self.id, self.db)
[172]653                except util.TracError, detail:
[253]654                        # Not a valid ticket
655                        self.id = None
[172]656                        return False
[126]657
[220]658                # reopen the ticket if it is was closed
659                # We must use the ticket workflow framework
660                #
661                if tkt['status'] in ['closed']:
662                        tkt['status'] = 'reopened'
663                        tkt['resolution'] = ''
664
[172]665                # Must we update some ticket fields properties
666                #
[220]667                if update_fields:
668                        self.update_ticket_fields(tkt, update_fields)
[166]669
[236]670                message_parts = self.get_message_parts(m)
[253]671                message_parts = self.unique_attachment_names(message_parts)
[210]672
[177]673                if self.EMAIL_HEADER:
[236]674                        message_parts.insert(0, self.email_header_txt(m))
[76]675
[236]676                body_text = self.body_text(message_parts)
677
[241]678                if body_text.strip() or update_fields:
[250]679                        if self.DRY_RUN:
680                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
681                        else:
682                                tkt.save_changes(self.author, body_text, when)
[219]683
[172]684
[129]685                if self.VERSION  == 0.9:
[253]686                        str = self.attachments(message_parts, True)
[129]687                else:
[253]688                        str = self.attachments(message_parts)
[76]689
[204]690                if self.notification and not spam:
[253]691                        self.notify(tkt, False, when)
[72]692
[71]693                return True
694
[202]695        def set_ticket_fields(self, ticket):
[77]696                """
[202]697                set the ticket fields to value specified
698                        - /etc/email2trac.conf with <prefix>_<field>
699                        - trac default values, trac.ini
700                """
701                user_dict = dict()
702
703                for field in ticket.fields:
704
705                        name = field['name']
706
[215]707                        # skip some fields like resolution
708                        #
709                        if name in [ 'resolution' ]:
710                                continue
711
[202]712                        # default trac value
713                        #
[233]714                        if not field.get('custom'):
715                                value = self.get_config('ticket', 'default_%s' %(name) )
716                        else:
717                                value = field.get('value')
718                                options = field.get('options')
[234]719                                if value and options and value not in options:
[233]720                                        value = options[int(value)]
721
[202]722                        if self.DEBUG > 10:
723                                print 'trac.ini name %s = %s' %(name, value)
724
[206]725                        prefix = self.parameters['ticket_prefix']
[202]726                        try:
[206]727                                value = self.parameters['%s_%s' %(prefix, name)]
[202]728                                if self.DEBUG > 10:
729                                        print 'email2trac.conf %s = %s ' %(name, value)
730
731                        except KeyError, detail:
732                                pass
733               
734                        if self.DEBUG:
735                                print 'user_dict[%s] = %s' %(name, value)
736
737                        user_dict[name] = value
738
739                self.update_ticket_fields(ticket, user_dict, use_default=1)
740
741                # Set status ticket
742                #`
743                ticket['status'] = 'new'
744
745
746
[204]747        def new_ticket(self, msg, spam):
[202]748                """
[77]749                Create a new ticket
750                """
[250]751                if self.DEBUG:
752                        print "TD: new_ticket"
753
[41]754                tkt = Ticket(self.env)
[202]755                self.set_ticket_fields(tkt)
756
757                # Old style setting for component, will be removed
758                #
[204]759                if spam:
760                        tkt['component'] = 'Spam'
761
[206]762                elif self.parameters.has_key('component'):
763                        tkt['component'] = self.parameters['component']
[201]764
[22]765                if not msg['Subject']:
[151]766                        tkt['summary'] = u'(No subject)'
[22]767                else:
[139]768                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]769
[72]770                self.set_reply_fields(tkt, msg)
[22]771
[45]772                # produce e-mail like header
773                #
[22]774                head = ''
775                if self.EMAIL_HEADER > 0:
776                        head = self.email_header_txt(msg)
[92]777                       
[236]778                message_parts = self.get_message_parts(msg)
779                message_parts = self.unique_attachment_names(message_parts)
780               
781                if self.EMAIL_HEADER > 0:
782                        message_parts.insert(0, self.email_header_txt(msg))
783                       
784                body_text = self.body_text(message_parts)
[45]785
[236]786                tkt['description'] = body_text
[90]787
[182]788                #when = int(time.time())
[192]789                #
[182]790                utc = UTC()
791                when = datetime.now(utc)
[45]792
[253]793                if not self.DRY_RUN:
794                        self.id = tkt.insert()
[250]795               
[90]796                changed = False
797                comment = ''
[77]798
[90]799                # Rewrite the description if we have mailto enabled
[45]800                #
[72]801                if self.MAILTO:
[100]802                        changed = True
[142]803                        comment = u'\nadded mailto line\n'
[253]804                        mailto = self.html_mailto_link( m['Subject'], body_text)
805
[213]806                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]807                                %(head, mailto, body_text)
[45]808
[253]809                str =  self.attachments(message_parts)
[152]810                if str:
[100]811                        changed = True
[152]812                        comment = '%s\n%s\n' %(comment, str)
[77]813
[90]814                if changed:
[204]815                        if self.DRY_RUN:
[250]816                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
[201]817                        else:
818                                tkt.save_changes(self.author, comment)
819                                #print tkt.get_changelog(self.db, when)
[90]820
[250]821                if self.notification and not spam:
[253]822                        self.notify(tkt, True)
[45]823
[77]824        def parse(self, fp):
[96]825                global m
826
[77]827                m = email.message_from_file(fp)
[239]828               
[77]829                if not m:
[221]830                        if self.DEBUG:
[250]831                                print "TD: This is not a valid email message format"
[77]832                        return
[239]833                       
834                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
835                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
836
[77]837                if self.DEBUG > 1:        # save the entire e-mail message text
[236]838                        message_parts = self.get_message_parts(m)
839                        message_parts = self.unique_attachment_names(message_parts)
[219]840                        self.save_email_for_debug(m, True)
[236]841                        body_text = self.body_text(message_parts)
842                        self.debug_body(body_text, True)
843                        self.debug_attachments(message_parts)
[77]844
845                self.db = self.env.get_db_cnx()
[194]846                self.get_sender_info(m)
[152]847
[221]848                if not self.email_header_acl('white_list', self.email_addr, True):
849                        if self.DEBUG > 1 :
850                                print 'Message rejected : %s not in white list' %(self.email_addr)
851                        return False
[77]852
[221]853                if self.email_header_acl('black_list', self.email_addr, False):
854                        if self.DEBUG > 1 :
855                                print 'Message rejected : %s in black list' %(self.email_addr)
856                        return False
857
[227]858                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]859                        if self.DEBUG > 1 :
860                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
861                        return False
862
[204]863                # If drop the message
[194]864                #
[204]865                if self.spam(m) == 'drop':
[194]866                        return False
867
[204]868                elif self.spam(m) == 'spam':
869                        spam_msg = True
[194]870
[204]871                else:
872                        spam_msg = False
873
[77]874                if self.get_config('notification', 'smtp_enabled') in ['true']:
875                        self.notification = 1
876                else:
877                        self.notification = 0
878
879                # Must we update existing tickets
880                #
881                if self.TICKET_UPDATE > 0:
[204]882                        if self.ticket_update(m, spam_msg):
[77]883                                return True
884
[204]885                self.new_ticket(m, spam_msg)
[77]886
[136]887        def strip_signature(self, text):
888                """
889                Strip signature from message, inspired by Mailman software
890                """
891                body = []
892                for line in text.splitlines():
893                        if line == '-- ':
894                                break
895                        body.append(line)
896
897                return ('\n'.join(body))
898
[231]899        def reflow(self, text, delsp = 0):
900                """
901                Reflow the message based on the format="flowed" specification (RFC 3676)
902                """
903                flowedlines = []
904                quotelevel = 0
905                prevflowed = 0
906
907                for line in text.splitlines():
908                        from re import match
909                       
910                        # Figure out the quote level and the content of the current line
911                        m = match('(>*)( ?)(.*)', line)
912                        linequotelevel = len(m.group(1))
913                        line = m.group(3)
914
915                        # Determine whether this line is flowed
916                        if line and line != '-- ' and line[-1] == ' ':
917                                flowed = 1
918                        else:
919                                flowed = 0
920
921                        if flowed and delsp and line and line[-1] == ' ':
922                                line = line[:-1]
923
924                        # If the previous line is flowed, append this line to it
925                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
926                                flowedlines[-1] += line
927                        # Otherwise, start a new line
928                        else:
929                                flowedlines.append('>' * linequotelevel + line)
930
931                        prevflowed = flowed
932                       
933
934                return '\n'.join(flowedlines)
935
[191]936        def strip_quotes(self, text):
[193]937                """
938                Strip quotes from message by Nicolas Mendoza
939                """
940                body = []
941                for line in text.splitlines():
942                        if line.startswith(self.EMAIL_QUOTE):
943                                continue
944                        body.append(line)
[151]945
[193]946                return ('\n'.join(body))
[191]947
[154]948        def wrap_text(self, text, replace_whitespace = False):
[151]949                """
[191]950                Will break a lines longer then given length into several small
951                lines of size given length
[151]952                """
953                import textwrap
[154]954
[151]955                LINESEPARATOR = '\n'
[153]956                reformat = ''
[151]957
[154]958                for s in text.split(LINESEPARATOR):
959                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
960                        if tmp:
961                                reformat = '%s\n%s' %(reformat,tmp)
962                        else:
963                                reformat = '%s\n' %reformat
[153]964
965                return reformat
966
[154]967                # Python2.4 and higher
968                #
969                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
970                #
971
972
[236]973        def get_message_parts(self, msg):
[45]974                """
[236]975                parses the email message and returns a list of body parts and attachments
976                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]977                """
[236]978                message_parts = []
[238]979               
980                # This is used to figure out when we are inside an AppleDouble container
981                # AppleDouble containers consists of two parts: Mac-specific file data, and platform-independent data
982                # We strip away Mac-specific stuff
983                appledouble_parts = []
[236]984
[22]985                for part in msg.walk():
[236]986                        if self.DEBUG:
987                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[238]988                               
989                        # Check whether we just finished processing an AppleDouble container
990                        if part not in appledouble_parts:
991                                appledouble_parts = []
[236]992
[238]993                        # Special handling for BinHex attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
994                        if part.get_content_type() == 'application/mac-binhex40':
995                                if self.BINHEX == 'warn':
996                                        message_parts.append("'''A BinHex attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
997                                        continue
998                                elif self.BINHEX == 'drop':
999                                        continue
1000
1001                        # Special handling for AppleSingle attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1002                        if part.get_content_type() == 'application/applefile' and not part in appledouble_parts:
1003                                if self.APPLESINGLE == 'warn':
1004                                        message_parts.append("'''An AppleSingle attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1005                                        continue
1006                                elif self.APPLESINGLE == 'drop':
1007                                        continue
1008
1009                        # Special handling for the Mac-specific part of AppleDouble attachments. Options are strip (leave out with no warning), warn (and leave out), and keep
1010                        if part.get_content_type() == 'application/applefile':
1011                                if self.APPLEDOUBLE == 'warn':
1012                                        message_parts.append("'''The resource fork of an attachment named '%s' was removed.'''" % part.get_filename())
1013                                        continue
1014                                elif self.APPLEDOUBLE == 'strip':
1015                                        continue
1016
1017                        # If we entering an AppleDouble container, set up appledouble_parts so that we know what to do with its subparts
1018                        if part.get_content_type() == 'multipart/appledouble':
1019                                appledouble_parts = part.get_payload()
1020                                continue
1021
1022                        # Any other multipart/* is just a container for multipart messages
[45]1023                        if part.get_content_maintype() == 'multipart':
[22]1024                                continue
1025
[236]1026                        # Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1027                        inline = self.inline_part(part)
1028
1029                        # Inline text parts are where the body is
1030                        if part.get_content_type() == 'text/plain' and inline:
1031                                if self.DEBUG:
1032                                        print 'TD:               Inline body part'
1033
[45]1034                                # Try to decode, if fails then do not decode
1035                                #
[90]1036                                body_text = part.get_payload(decode=1)
[45]1037                                if not body_text:                       
[90]1038                                        body_text = part.get_payload(decode=0)
[231]1039
[232]1040                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1041                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1042
1043                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1044                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1045       
[136]1046                                if self.STRIP_SIGNATURE:
1047                                        body_text = self.strip_signature(body_text)
[22]1048
[191]1049                                if self.STRIP_QUOTES:
1050                                        body_text = self.strip_quotes(body_text)
1051
[148]1052                                if self.USE_TEXTWRAP:
[151]1053                                        body_text = self.wrap_text(body_text)
[148]1054
[45]1055                                # Get contents charset (iso-8859-15 if not defined in mail headers)
1056                                #
[100]1057                                charset = part.get_content_charset()
[102]1058                                if not charset:
1059                                        charset = 'iso-8859-15'
1060
[89]1061                                try:
[96]1062                                        ubody_text = unicode(body_text, charset)
[100]1063
1064                                except UnicodeError, detail:
[96]1065                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1066
[100]1067                                except LookupError, detail:
[139]1068                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1069
[236]1070                                if self.VERBATIM_FORMAT:
1071                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1072                                else:
1073                                        message_parts.append('%s' %ubody_text)
1074                        else:
1075                                if self.DEBUG:
1076                                        print 'TD:               Filename: %s' % part.get_filename()
[22]1077
[236]1078                                message_parts.append((part.get_filename(), part))
1079
1080                return message_parts
1081               
[253]1082        def unique_attachment_names(self, message_parts):
[236]1083                renamed_parts = []
1084                attachment_names = set()
1085                for part in message_parts:
1086                       
1087                        # If not an attachment, leave it alone
1088                        if not isinstance(part, tuple):
1089                                renamed_parts.append(part)
1090                                continue
1091                               
1092                        (filename, part) = part
1093                        # Decode the filename
1094                        if filename:
1095                                filename = self.email_to_unicode(filename)                     
1096                        # If no name, use a default one
[22]1097                        else:
[236]1098                                filename = 'untitled-part'
[22]1099
[242]1100                                # Guess the extension from the content type, use non strict mode
1101                                # some additional non-standard but commonly used MIME types
1102                                # are also recognized
1103                                #
1104                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1105                                if not ext:
1106                                        ext = '.bin'
[22]1107
[236]1108                                filename = '%s%s' % (filename, ext)
[22]1109
[236]1110                        # Discard relative paths in attachment names
1111                        filename = filename.replace('\\', '/').replace(':', '/')
1112                        filename = os.path.basename(filename)
[22]1113
[236]1114                        # We try to normalize the filename to utf-8 NFC if we can.
1115                        # Files uploaded from OS X might be in NFD.
1116                        # Check python version and then try it
1117                        #
1118                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1119                                try:
1120                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1121                                except TypeError:
1122                                        pass
1123                                       
1124                        if self.QUOTE_ATTACHMENT_FILENAMES:
1125                                filename = urllib.quote(filename)
[100]1126
[236]1127                        # Make the filename unique for this ticket
1128                        num = 0
1129                        unique_filename = filename
1130                        filename, ext = os.path.splitext(filename)
[134]1131
[253]1132                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
[236]1133                                num += 1
1134                                unique_filename = "%s-%s%s" % (filename, num, ext)
1135                               
1136                        if self.DEBUG:
1137                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[100]1138
[236]1139                        attachment_names.add(unique_filename)
1140
1141                        renamed_parts.append((filename, unique_filename, part))
1142               
1143                return renamed_parts
1144                       
1145        def inline_part(self, part):
1146                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1147               
1148                       
[253]1149        def attachment_exists(self, filename):
[250]1150
1151                if self.DEBUG:
[253]1152                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
[250]1153
1154                # We have no valid ticket id
1155                #
[253]1156                if not self.id:
[236]1157                        return False
[250]1158
[236]1159                try:
[253]1160                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
[236]1161                        return True
[250]1162                except attachment.ResourceNotFound:
[236]1163                        return False
1164                       
1165        def body_text(self, message_parts):
1166                body_text = []
1167               
1168                for part in message_parts:
1169                        # Plain text part, append it
1170                        if not isinstance(part, tuple):
1171                                body_text.extend(part.strip().splitlines())
1172                                body_text.append("")
1173                                continue
1174                               
1175                        (original, filename, part) = part
1176                        inline = self.inline_part(part)
1177                       
1178                        if part.get_content_maintype() == 'image' and inline:
1179                                body_text.append('[[Image(%s)]]' % filename)
1180                                body_text.append("")
1181                        else:
1182                                body_text.append('[attachment:"%s"]' % filename)
1183                                body_text.append("")
1184                               
1185                body_text = '\r\n'.join(body_text)
1186                return body_text
1187
[253]1188        def notify(self, tkt, new=True, modtime=0):
[79]1189                """
1190                A wrapper for the TRAC notify function. So we can use templates
1191                """
[250]1192                if self.DRY_RUN:
1193                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1194                                return
[41]1195                try:
1196                        # create false {abs_}href properties, to trick Notify()
1197                        #
[193]1198                        if not self.VERSION == 0.11:
[192]1199                                self.env.abs_href = Href(self.get_config('project', 'url'))
1200                                self.env.href = Href(self.get_config('project', 'url'))
[22]1201
[41]1202                        tn = TicketNotifyEmail(self.env)
[213]1203
[42]1204                        if self.notify_template:
[222]1205
[221]1206                                if self.VERSION == 0.11:
[222]1207
[221]1208                                        from trac.web.chrome import Chrome
[222]1209
1210                                        if self.notify_template_update and not new:
1211                                                tn.template_name = self.notify_template_update
1212                                        else:
1213                                                tn.template_name = self.notify_template
1214
[221]1215                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1216                                               
1217                                else:
[222]1218
[221]1219                                        tn.template_name = self.notify_template;
[42]1220
[77]1221                        tn.notify(tkt, new, modtime)
[41]1222
1223                except Exception, e:
[253]1224                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
[41]1225
[253]1226        def html_mailto_link(self, subject, body):
1227                """
1228                This function returns a HTML mailto tag with the ticket id and author email address
1229                """
[72]1230                if not self.author:
[143]1231                        author = self.email_addr
[22]1232                else:   
[142]1233                        author = self.author
[22]1234
[253]1235                # use urllib to escape the chars
[22]1236                #
[74]1237                str = 'mailto:%s?Subject=%s&Cc=%s' %(
1238                       urllib.quote(self.email_addr),
[253]1239                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
[74]1240                           urllib.quote(self.MAILTO_CC)
1241                           )
1242
[213]1243                str = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(str, author)
[22]1244                return str
1245
[253]1246        def attachments(self, message_parts, update=False):
[79]1247                '''
1248                save any attachments as files in the ticket's directory
1249                '''
[237]1250                if self.DRY_RUN:
[250]1251                        print "DRY_RUN: no attachments saved"
[237]1252                        return ''
1253
[22]1254                count = 0
[152]1255
1256                # Get Maxium attachment size
1257                #
1258                max_size = int(self.get_config('attachment', 'max_size'))
[153]1259                status   = ''
[236]1260               
1261                for part in message_parts:
1262                        # Skip body parts
1263                        if not isinstance(part, tuple):
[22]1264                                continue
[236]1265                               
1266                        (original, filename, part) = part
[48]1267                        #
[172]1268                        # Must be tuneables HvB
1269                        #
[236]1270                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
[22]1271                        text = part.get_payload(decode=1)
1272                        if not text:
1273                                text = '(None)'
[48]1274                        fd.write(text)
1275                        fd.close()
[22]1276
[153]1277                        # get the file_size
[22]1278                        #
[48]1279                        stats = os.lstat(path)
[153]1280                        file_size = stats[stat.ST_SIZE]
[22]1281
[152]1282                        # Check if the attachment size is allowed
1283                        #
[153]1284                        if (max_size != -1) and (file_size > max_size):
1285                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
[236]1286                                        %(status, original, file_size, max_size)
[152]1287
1288                                os.unlink(path)
1289                                continue
1290                        else:
1291                                count = count + 1
1292                                       
[172]1293                        # Insert the attachment
[73]1294                        #
[242]1295                        fd = open(path, 'rb')
[253]1296                        att = attachment.Attachment(self.env, 'ticket', self.id)
[73]1297
[172]1298                        # This will break the ticket_update system, the body_text is vaporized
1299                        # ;-(
1300                        #
1301                        if not update:
1302                                att.author = self.author
1303                                att.description = self.email_to_unicode('Added by email2trac')
[73]1304
[236]1305                        att.insert(filename, fd, file_size)
[172]1306                        #except  util.TracError, detail:
1307                        #       print detail
[73]1308
[103]1309                        # Remove the created temporary filename
1310                        #
[172]1311                        fd.close()
[103]1312                        os.unlink(path)
1313
[77]1314                # Return how many attachments
1315                #
[153]1316                status = 'This message has %d attachment(s)\n%s' %(count, status)
1317                return status
[22]1318
[77]1319
[22]1320def mkdir_p(dir, mode):
1321        '''do a mkdir -p'''
1322
1323        arr = string.split(dir, '/')
1324        path = ''
1325        for part in arr:
1326                path = '%s/%s' % (path, part)
1327                try:
1328                        stats = os.stat(path)
1329                except OSError:
1330                        os.mkdir(path, mode)
1331
1332def ReadConfig(file, name):
1333        """
1334        Parse the config file
1335        """
1336        if not os.path.isfile(file):
[79]1337                print 'File %s does not exist' %file
[22]1338                sys.exit(1)
1339
[199]1340        config = trac_config.Configuration(file)
[22]1341
1342        # Use given project name else use defaults
1343        #
1344        if name:
[199]1345                sections = config.sections()
1346                if not name in sections:
[79]1347                        print "Not a valid project name: %s" %name
[199]1348                        print "Valid names: %s" %sections
[22]1349                        sys.exit(1)
1350
1351                project =  dict()
[199]1352                for option, value in  config.options(name):
1353                        project[option] = value
[22]1354
1355        else:
[217]1356                # use some trac internales to get the defaults
1357                #
1358                project = config.parser.defaults()
[22]1359
1360        return project
1361
[87]1362
[22]1363if __name__ == '__main__':
1364        # Default config file
1365        #
[24]1366        configfile = '@email2trac_conf@'
[22]1367        project = ''
1368        component = ''
[202]1369        ticket_prefix = 'default'
[204]1370        dry_run = None
[202]1371
[87]1372        ENABLE_SYSLOG = 0
[201]1373
[204]1374
[202]1375        SHORT_OPT = 'chf:np:t:'
1376        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
[201]1377
[22]1378        try:
[201]1379                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1380        except getopt.error,detail:
1381                print __doc__
1382                print detail
1383                sys.exit(1)
[87]1384       
[22]1385        project_name = None
1386        for opt,value in opts:
1387                if opt in [ '-h', '--help']:
1388                        print __doc__
1389                        sys.exit(0)
1390                elif opt in ['-c', '--component']:
1391                        component = value
1392                elif opt in ['-f', '--file']:
1393                        configfile = value
[201]1394                elif opt in ['-n', '--dry-run']:
[204]1395                        dry_run = True
[22]1396                elif opt in ['-p', '--project']:
1397                        project_name = value
[202]1398                elif opt in ['-t', '--ticket_prefix']:
1399                        ticket_prefix = value
[87]1400       
[22]1401        settings = ReadConfig(configfile, project_name)
1402        if not settings.has_key('project'):
1403                print __doc__
[79]1404                print 'No Trac project is defined in the email2trac config file.'
[22]1405                sys.exit(1)
[87]1406       
[22]1407        if component:
1408                settings['component'] = component
[202]1409
1410        # The default prefix for ticket values in email2trac.conf
1411        #
1412        settings['ticket_prefix'] = ticket_prefix
[206]1413        settings['dry_run'] = dry_run
[87]1414       
[22]1415        if settings.has_key('trac_version'):
[189]1416                version = settings['trac_version']
[22]1417        else:
1418                version = trac_default_version
1419
[189]1420
[22]1421        #debug HvB
1422        #print settings
[189]1423
[87]1424        try:
[189]1425                if version == '0.9':
[87]1426                        from trac import attachment
1427                        from trac.env import Environment
1428                        from trac.ticket import Ticket
1429                        from trac.web.href import Href
1430                        from trac import util
1431                        from trac.Notify import TicketNotifyEmail
[189]1432                elif version == '0.10':
[87]1433                        from trac import attachment
1434                        from trac.env import Environment
1435                        from trac.ticket import Ticket
1436                        from trac.web.href import Href
1437                        from trac import util
[139]1438                        #
1439                        # return  util.text.to_unicode(str)
1440                        #
[87]1441                        # see http://projects.edgewall.com/trac/changeset/2799
1442                        from trac.ticket.notification import TicketNotifyEmail
[199]1443                        from trac import config as trac_config
[189]1444                elif version == '0.11':
[182]1445                        from trac import attachment
1446                        from trac.env import Environment
1447                        from trac.ticket import Ticket
1448                        from trac.web.href import Href
[199]1449                        from trac import config as trac_config
[182]1450                        from trac import util
1451                        #
1452                        # return  util.text.to_unicode(str)
1453                        #
1454                        # see http://projects.edgewall.com/trac/changeset/2799
1455                        from trac.ticket.notification import TicketNotifyEmail
[189]1456                else:
1457                        print 'TRAC version %s is not supported' %version
1458                        sys.exit(1)
1459                       
1460                if settings.has_key('enable_syslog'):
[190]1461                        if SYSLOG_AVAILABLE:
1462                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1463
[87]1464                env = Environment(settings['project'], create=0)
[206]1465                tktparser = TicketEmailParser(env, settings, float(version))
[87]1466                tktparser.parse(sys.stdin)
[22]1467
[87]1468        # Catch all errors ans log to SYSLOG if we have enabled this
1469        # else stdout
1470        #
1471        except Exception, error:
1472                if ENABLE_SYSLOG:
1473                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1474
[87]1475                        etype, evalue, etb = sys.exc_info()
1476                        for e in traceback.format_exception(etype, evalue, etb):
1477                                syslog.syslog(e)
[187]1478
[87]1479                        syslog.closelog()
1480                else:
1481                        traceback.print_exc()
[22]1482
[97]1483                if m:
[98]1484                        tktparser.save_email_for_debug(m, True)
[97]1485
[249]1486                sys.exit(1)
[22]1487# EOB
Note: See TracBrowser for help on using the repository browser.