source: trunk/email2trac.py.in @ 252

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

email2trac.py.in:

  • always use self.author = self.email_addr, #118

debian/changelog:

  • new version
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 38.1 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 252 2009-01-27 15:08:19Z 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
[72]148
[22]149                self.VERSION = version
[206]150                self.DRY_RUN = parameters['dry_run']
[204]151
[172]152                self.get_config = self.env.config.get
[22]153
154                if parameters.has_key('umask'):
155                        os.umask(int(parameters['umask'], 8))
156
[236]157                if parameters.has_key('quote_attachment_filenames'):
158                        self.QUOTE_ATTACHMENT_FILENAMES = int(parameters['quote_attachment_filenames'])
159                else:
160                        self.QUOTE_ATTACHMENT_FILENAMES = 1
161
[22]162                if parameters.has_key('debug'):
163                        self.DEBUG = int(parameters['debug'])
164                else:
165                        self.DEBUG = 0
166
167                if parameters.has_key('mailto_link'):
168                        self.MAILTO = int(parameters['mailto_link'])
[74]169                        if parameters.has_key('mailto_cc'):
170                                self.MAILTO_CC = parameters['mailto_cc']
171                        else:
172                                self.MAILTO_CC = ''
[22]173                else:
174                        self.MAILTO = 0
175
176                if parameters.has_key('spam_level'):
177                        self.SPAM_LEVEL = int(parameters['spam_level'])
178                else:
179                        self.SPAM_LEVEL = 0
180
[207]181                if parameters.has_key('spam_header'):
182                        self.SPAM_HEADER = parameters['spam_header']
183                else:
184                        self.SPAM_HEADER = 'X-Spam-Score'
185
[191]186                if parameters.has_key('email_quote'):
187                        self.EMAIL_QUOTE = str(parameters['email_quote'])
188                else:   
189                        self.EMAIL_QUOTE = '> '
[22]190
191                if parameters.has_key('email_header'):
192                        self.EMAIL_HEADER = int(parameters['email_header'])
193                else:
194                        self.EMAIL_HEADER = 0
195
[42]196                if parameters.has_key('alternate_notify_template'):
197                        self.notify_template = str(parameters['alternate_notify_template'])
198                else:
199                        self.notify_template = None
[22]200
[222]201                if parameters.has_key('alternate_notify_template_update'):
202                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
203                else:
204                        self.notify_template_update = None
205
[43]206                if parameters.has_key('reply_all'):
207                        self.REPLY_ALL = int(parameters['reply_all'])
208                else:
209                        self.REPLY_ALL = 0
[42]210
[74]211                if parameters.has_key('ticket_update'):
212                        self.TICKET_UPDATE = int(parameters['ticket_update'])
213                else:
214                        self.TICKET_UPDATE = 0
[43]215
[118]216                if parameters.has_key('drop_spam'):
217                        self.DROP_SPAM = int(parameters['drop_spam'])
218                else:
219                        self.DROP_SPAM = 0
[74]220
[134]221                if parameters.has_key('verbatim_format'):
222                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
223                else:
224                        self.VERBATIM_FORMAT = 1
[118]225
[231]226                if parameters.has_key('reflow'):
227                        self.REFLOW = int(parameter['reflow'])
228                else:
229                        self.REFLOW = 1
230
[136]231                if parameters.has_key('strip_signature'):
232                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
233                else:
234                        self.STRIP_SIGNATURE = 0
[134]235
[191]236                if parameters.has_key('strip_quotes'):
237                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
238                else:
239                        self.STRIP_QUOTES = 0
240
[148]241                if parameters.has_key('use_textwrap'):
242                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
243                else:
244                        self.USE_TEXTWRAP = 0
245
[238]246                if parameters.has_key('binhex'):
247                        self.BINHEX = parameters['binhex']
248                else:
249                        self.BINHEX = 'warn'
250
251                if parameters.has_key('applesingle'):
252                        self.APPLESINGLE = parameters['applesingle']
253                else:
254                        self.APPLESINGLE = 'warn'
255
256                if parameters.has_key('appledouble'):
257                        self.APPLEDOUBLE = parameters['appledouble']
258                else:
259                        self.APPLEDOUBLE = 'warn'
260
[163]261                if parameters.has_key('python_egg_cache'):
262                        self.python_egg_cache = str(parameters['python_egg_cache'])
263                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
264
[173]265                # Use OS independend functions
266                #
267                self.TMPDIR = os.path.normcase('/tmp')
268                if parameters.has_key('tmpdir'):
269                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
270
[194]271                if parameters.has_key('ignore_trac_user_settings'):
272                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
273                else:
274                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]275
[22]276        def spam(self, message):
[191]277                """
278                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
279                # Note if Spam_level then '*' are included
280                """
[194]281                spam = False
[207]282                if message.has_key(self.SPAM_HEADER):
283                        spam_l = string.split(message[self.SPAM_HEADER])
[22]284
[207]285                        try:
286                                number = spam_l[0].count('*')
287                        except IndexError, detail:
288                                number = 0
289                               
[22]290                        if number >= self.SPAM_LEVEL:
[194]291                                spam = True
292                               
[191]293                # treat virus mails as spam
294                #
295                elif message.has_key('X-Virus-found'):                 
[194]296                        spam = True
297
298                # How to handle SPAM messages
299                #
300                if self.DROP_SPAM and spam:
301                        if self.DEBUG > 2 :
302                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
303
[204]304                        return 'drop'   
[194]305
306                elif spam:
307
[204]308                        return 'Spam'   
[67]309
[194]310                else:
[22]311
[204]312                        return False
[191]313
[221]314        def email_header_acl(self, keyword, header_field, default):
[206]315                """
[221]316                This function wil check if the email address is allowed or denied
317                to send mail to the ticket list
318            """
[206]319                try:
[221]320                        mail_addresses = self.parameters[keyword]
321
322                        # Check if we have an empty string
323                        #
324                        if not mail_addresses:
325                                return default
326
[206]327                except KeyError, detail:
[221]328                        if self.DEBUG > 2 :
[250]329                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
[206]330
[221]331                        return default
[206]332
[221]333                mail_addresses = string.split(mail_addresses, ',')
334
335                for entry in mail_addresses:
[209]336                        entry = entry.strip()
[221]337                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
338                        result =  TO_RE.search(header_field)
[208]339                        if result:
340                                return True
[149]341
[208]342                return False
343
[139]344        def email_to_unicode(self, message_str):
[22]345                """
346                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]347        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]348                understands it.
349                """
[139]350                results =  email.Header.decode_header(message_str)
[22]351                str = None
352                for text,format in results:
353                        if format:
354                                try:
355                                        temp = unicode(text, format)
[139]356                                except UnicodeError, detail:
[22]357                                        # This always works
358                                        #
359                                        temp = unicode(text, 'iso-8859-15')
[139]360                                except LookupError, detail:
361                                        #text = 'ERROR: Could not find charset: %s, please install' %format
362                                        #temp = unicode(text, 'iso-8859-15')
363                                        temp = message_str
364                                       
[22]365                        else:
366                                temp = string.strip(text)
[92]367                                temp = unicode(text, 'iso-8859-15')
[22]368
369                        if str:
[100]370                                str = '%s %s' %(str, temp)
[22]371                        else:
[100]372                                str = '%s' %temp
[22]373
[139]374                #str = str.encode('utf-8')
[22]375                return str
376
[236]377        def debug_body(self, message_body, tempfile=False):
378                if tempfile:
379                        import tempfile
380                        body_file = tempfile.mktemp('.email2trac')
381                else:
382                        body_file = os.path.join(self.TMPDIR, 'body.txt')
383
384                print 'TD: writing body (%s)' % body_file
385                fx = open(body_file, 'wb')
386                if not message_body:
387                        message_body = '(None)'
388                fx.write(message_body)
389                fx.close()
390                try:
391                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
392                except OSError:
393                        pass
394
395        def debug_attachments(self, message_parts):
[22]396                n = 0
[236]397                for part in message_parts:
398                        # Skip inline text parts
399                        if not isinstance(part, tuple):
[22]400                                continue
[236]401                               
[237]402                        (original, filename, part) = part
[22]403
404                        n = n + 1
405                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
406                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
407
[236]408                        part_file = os.path.join(self.TMPDIR, filename)
[173]409                        #part_file = '/var/tmp/part%d' % n
[22]410                        print 'TD: writing part%d (%s)' % (n,part_file)
411                        fx = open(part_file, 'wb')
412                        text = part.get_payload(decode=1)
413                        if not text:
414                                text = '(None)'
415                        fx.write(text)
416                        fx.close()
417                        try:
418                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
419                        except OSError:
420                                pass
421
422        def email_header_txt(self, m):
[72]423                """
424                Display To and CC addresses in description field
425                """
[22]426                str = ''
[213]427                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
428                if m['To'] and len(m['To']) > 0:
429                        str = "'''To:''' %s\r\n" %(m['To'])
[22]430                if m['Cc'] and len(m['Cc']) > 0:
[213]431                        str = "%s'''Cc:''' %s\r\n" % (str, m['Cc'])
[22]432
[139]433                return  self.email_to_unicode(str)
[22]434
[138]435
[194]436        def get_sender_info(self, message):
[45]437                """
[72]438                Get the default author name and email address from the message
[226]439                """
[43]440
[226]441                self.email_to = self.email_to_unicode(message['to'])
442                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
443
[194]444                self.email_from = self.email_to_unicode(message['from'])
[223]445                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]446
[252]447                # Trac can not handle author's name that contains spaces
448                #
449                self.author = self.email_addr
[194]450
451                if self.IGNORE_TRAC_USER_SETTINGS:
452                        return
453
454                # Is this a registered user, use email address as search key:
455                # result:
456                #   u : login name
457                #   n : Name that the user has set in the settings tab
458                #   e : email address that the user has set in the settings tab
[45]459                #
[194]460                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]461                        if e and (e.lower() == self.email_addr.lower()) ]
[43]462
[45]463                if len(users) == 1:
[194]464                        self.email_from = users[0][0]
[250]465                        self.author = users[0][0]
[45]466
[72]467        def set_reply_fields(self, ticket, message):
468                """
469                Set all the right fields for a new ticket
470                """
[183]471                ticket['reporter'] = self.email_from
[72]472
[45]473                # Put all CC-addresses in ticket CC field
[43]474                #
475                if self.REPLY_ALL:
[45]476                        #tos = message.get_all('to', [])
[43]477                        ccs = message.get_all('cc', [])
478
[45]479                        addrs = email.Utils.getaddresses(ccs)
[105]480                        if not addrs:
481                                return
[43]482
483                        # Remove reporter email address if notification is
484                        # on
485                        #
486                        if self.notification:
487                                try:
[72]488                                        addrs.remove((self.author, self.email_addr))
[43]489                                except ValueError, detail:
490                                        pass
491
[45]492                        for name,mail in addrs:
[105]493                                try:
[108]494                                        mail_list = '%s, %s' %(mail_list, mail)
[230]495                                except UnboundLocalError, detail:
[105]496                                        mail_list = mail
[43]497
[105]498                        if mail_list:
[139]499                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]500
501        def save_email_for_debug(self, message, tempfile=False):
502                if tempfile:
503                        import tempfile
504                        msg_file = tempfile.mktemp('.email2trac')
505                else:
[173]506                        #msg_file = '/var/tmp/msg.txt'
507                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
508
[44]509                print 'TD: saving email to %s' % msg_file
510                fx = open(msg_file, 'wb')
511                fx.write('%s' % message)
512                fx.close()
513                try:
514                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
515                except OSError:
516                        pass
517
[167]518        def str_to_dict(self, str):
[164]519                """
520                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
521                """
522                # Skip the last ':' character
523                #
524                fields = string.split(str[:-1], ',')
525
526                result = dict()
527                for field in fields:
528                        try:
529                                index, value = string.split(field,'=')
[169]530
531                                # We can not change the description of a ticket via the subject
532                                # line. The description is the body of the email
533                                #
534                                if index.lower() in ['description']:
535                                        continue
536
[164]537                                if value:
[165]538                                        result[index.lower()] = value
[169]539
[164]540                        except ValueError:
541                                pass
542
[165]543                return result
[167]544
[202]545        def update_ticket_fields(self, ticket, user_dict, use_default=None):
546                """
547                This will update the ticket fields. It will check if the
548                given fields are known and if the right values are specified
549                It will only update the ticket field value:
[169]550                        - If the field is known
[202]551                        - If the value supplied is valid for the ticket field.
552                          If not then there are two options:
553                           1) Skip the value (use_default=None)
554                           2) Set default value for field (use_default=1)
[169]555                """
556
557                # Build a system dictionary from the ticket fields
558                # with field as index and option as value
559                #
560                sys_dict = dict()
561                for field in ticket.fields:
[167]562                        try:
[169]563                                sys_dict[field['name']] = field['options']
564
[167]565                        except KeyError:
[169]566                                sys_dict[field['name']] = None
[167]567                                pass
[169]568
569                # Check user supplied fields an compare them with the
570                # system one's
571                #
572                for field,value in user_dict.items():
[202]573                        if self.DEBUG >= 10:
574                                print  'user_field\t %s = %s' %(field,value)
[169]575
576                        if sys_dict.has_key(field):
577
578                                # Check if value is an allowed system option, if TypeError then
579                                # every value is allowed
580                                #
581                                try:
582                                        if value in sys_dict[field]:
583                                                ticket[field] = value
[202]584                                        else:
585                                                # Must we set a default if value is not allowed
586                                                #
587                                                if use_default:
588                                                        value = self.get_config('ticket', 'default_%s' %(field) )
589                                                        ticket[field] = value
[169]590
591                                except TypeError:
592                                        ticket[field] = value
[202]593
594                                if self.DEBUG >= 10:
595                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]596                                       
[204]597        def ticket_update(self, m, spam):
[78]598                """
[79]599                If the current email is a reply to an existing ticket, this function
600                will append the contents of this email to that ticket, instead of
601                creating a new one.
[78]602                """
[250]603                if self.DEBUG:
604                        print "TD: ticket_update"
[202]605
[71]606                if not m['Subject']:
607                        return False
608                else:
[139]609                        subject  = self.email_to_unicode(m['Subject'])
[71]610
[182]611                # [hic] #1529: Re: LRZ
612                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
613                #
[71]614                TICKET_RE = re.compile(r"""
615                                        (?P<ticketnr>[#][0-9]+:)
[194]616                                        |(?P<ticketnr_fields>[#][\d]+\?.*?:)
[71]617                                        """, re.VERBOSE)
618
619                result =  TICKET_RE.search(subject)
620                if not result:
621                        return False
622
[164]623                # Must we update ticket fields
624                #
[220]625                update_fields = dict()
[165]626                try:
[164]627                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
[220]628                        update_fields = self.str_to_dict(keywords)
[165]629
630                        # Strip '#'
631                        #
632                        ticket_id = int(nr[1:])
633
634                except AttributeError:
635                        # Strip '#' and ':'
636                        #
[167]637                        nr = result.group('ticketnr')
[165]638                        ticket_id = int(nr[1:-1])
[164]639
[71]640
[194]641                # When is the change committed
642                #
[77]643                #
[194]644                if self.VERSION == 0.11:
645                        utc = UTC()
646                        when = datetime.now(utc)
647                else:
648                        when = int(time.time())
[77]649
[172]650                try:
651                        tkt = Ticket(self.env, ticket_id, self.db)
652                except util.TracError, detail:
653                        return False
[126]654
[220]655                # reopen the ticket if it is was closed
656                # We must use the ticket workflow framework
657                #
658                if tkt['status'] in ['closed']:
659                        tkt['status'] = 'reopened'
660                        tkt['resolution'] = ''
661
[172]662                # Must we update some ticket fields properties
663                #
[220]664                if update_fields:
665                        self.update_ticket_fields(tkt, update_fields)
[166]666
[236]667                message_parts = self.get_message_parts(m)
[250]668                message_parts = self.unique_attachment_names(message_parts, ticket_id)
[210]669
[177]670                if self.EMAIL_HEADER:
[236]671                        message_parts.insert(0, self.email_header_txt(m))
[76]672
[236]673                body_text = self.body_text(message_parts)
674
[241]675                if body_text.strip() or update_fields:
[250]676                        if self.DRY_RUN:
677                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
678                        else:
679                                tkt.save_changes(self.author, body_text, when)
[219]680
[172]681
[129]682                if self.VERSION  == 0.9:
[250]683                        str = self.attachments(message_parts, ticket_id, True)
[129]684                else:
[250]685                        str = self.attachments(message_parts, ticket_id)
[76]686
[204]687                if self.notification and not spam:
[250]688                        self.notify(tkt, ticket_id, False, when)
[72]689
[71]690                return True
691
[202]692        def set_ticket_fields(self, ticket):
[77]693                """
[202]694                set the ticket fields to value specified
695                        - /etc/email2trac.conf with <prefix>_<field>
696                        - trac default values, trac.ini
697                """
698                user_dict = dict()
699
700                for field in ticket.fields:
701
702                        name = field['name']
703
[215]704                        # skip some fields like resolution
705                        #
706                        if name in [ 'resolution' ]:
707                                continue
708
[202]709                        # default trac value
710                        #
[233]711                        if not field.get('custom'):
712                                value = self.get_config('ticket', 'default_%s' %(name) )
713                        else:
714                                value = field.get('value')
715                                options = field.get('options')
[234]716                                if value and options and value not in options:
[233]717                                        value = options[int(value)]
718
[202]719                        if self.DEBUG > 10:
720                                print 'trac.ini name %s = %s' %(name, value)
721
[206]722                        prefix = self.parameters['ticket_prefix']
[202]723                        try:
[206]724                                value = self.parameters['%s_%s' %(prefix, name)]
[202]725                                if self.DEBUG > 10:
726                                        print 'email2trac.conf %s = %s ' %(name, value)
727
728                        except KeyError, detail:
729                                pass
730               
731                        if self.DEBUG:
732                                print 'user_dict[%s] = %s' %(name, value)
733
734                        user_dict[name] = value
735
736                self.update_ticket_fields(ticket, user_dict, use_default=1)
737
738                # Set status ticket
739                #`
740                ticket['status'] = 'new'
741
742
743
[204]744        def new_ticket(self, msg, spam):
[202]745                """
[77]746                Create a new ticket
747                """
[250]748                if self.DEBUG:
749                        print "TD: new_ticket"
750
[41]751                tkt = Ticket(self.env)
[202]752                self.set_ticket_fields(tkt)
753
754                # Old style setting for component, will be removed
755                #
[204]756                if spam:
757                        tkt['component'] = 'Spam'
758
[206]759                elif self.parameters.has_key('component'):
760                        tkt['component'] = self.parameters['component']
[201]761
[22]762                if not msg['Subject']:
[151]763                        tkt['summary'] = u'(No subject)'
[22]764                else:
[139]765                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]766
[72]767                self.set_reply_fields(tkt, msg)
[22]768
[45]769                # produce e-mail like header
770                #
[22]771                head = ''
772                if self.EMAIL_HEADER > 0:
773                        head = self.email_header_txt(msg)
[92]774                       
[236]775                message_parts = self.get_message_parts(msg)
776                message_parts = self.unique_attachment_names(message_parts)
777               
778                if self.EMAIL_HEADER > 0:
779                        message_parts.insert(0, self.email_header_txt(msg))
780                       
781                body_text = self.body_text(message_parts)
[45]782
[236]783                tkt['description'] = body_text
[90]784
[182]785                #when = int(time.time())
[192]786                #
[182]787                utc = UTC()
788                when = datetime.now(utc)
[45]789
[204]790                if self.DRY_RUN:
[201]791                        ticket_id = 'DRY_RUN'
792                else:
793                        ticket_id = tkt.insert()
[250]794               
[90]795                changed = False
796                comment = ''
[77]797
[90]798                # Rewrite the description if we have mailto enabled
[45]799                #
[72]800                if self.MAILTO:
[100]801                        changed = True
[142]802                        comment = u'\nadded mailto line\n'
[210]803                        #mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
804                        mailto = self.html_mailto_link( m['Subject'], ticket_id, body_text)
[213]805                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]806                                %(head, mailto, body_text)
[45]807
[250]808                str =  self.attachments(message_parts, ticket_id)
[152]809                if str:
[100]810                        changed = True
[152]811                        comment = '%s\n%s\n' %(comment, str)
[77]812
[90]813                if changed:
[204]814                        if self.DRY_RUN:
[250]815                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
[201]816                        else:
817                                tkt.save_changes(self.author, comment)
818                                #print tkt.get_changelog(self.db, when)
[90]819
[250]820                if self.notification and not spam:
821                        self.notify(tkt, ticket_id, True)
822                        #self.notify(ticket_id, False)
[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               
[250]1082        def unique_attachment_names(self, message_parts, tkt_id = None):
[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
[250]1132                        while unique_filename in attachment_names or self.attachment_exists(tkt_id, 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                       
[250]1149        def attachment_exists(self, tkt_id, filename):
1150
1151                if self.DEBUG:
1152                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(tkt_id, filename)
1153
1154                # We have no valid ticket id
1155                #
1156                if not tkt_id:
[236]1157                        return False
[250]1158
[236]1159                try:
[250]1160                        att = attachment.Attachment(self.env, 'ticket', tkt_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
[250]1188        def notify(self, tkt, id, 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:
[250]1224                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(id, e)
[41]1225
[72]1226        def html_mailto_link(self, subject, id, body):
1227                if not self.author:
[143]1228                        author = self.email_addr
[22]1229                else:   
[142]1230                        author = self.author
[22]1231
1232                # Must find a fix
1233                #
1234                #arr = string.split(body, '\n')
1235                #arr = map(self.mail_line, arr)
1236                #body = string.join(arr, '\n')
1237                #body = '%s wrote:\n%s' %(author, body)
1238
1239                # Temporary fix
[142]1240                #
[74]1241                str = 'mailto:%s?Subject=%s&Cc=%s' %(
1242                       urllib.quote(self.email_addr),
1243                           urllib.quote('Re: #%s: %s' %(id, subject)),
1244                           urllib.quote(self.MAILTO_CC)
1245                           )
1246
[213]1247                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]1248                return str
1249
[250]1250        def attachments(self, message_parts, tkt_id, update=False):
[79]1251                '''
1252                save any attachments as files in the ticket's directory
1253                '''
[237]1254                if self.DRY_RUN:
[250]1255                        print "DRY_RUN: no attachments saved"
[237]1256                        return ''
1257
[22]1258                count = 0
[77]1259                first = 0
1260                number = 0
[152]1261
1262                # Get Maxium attachment size
1263                #
1264                max_size = int(self.get_config('attachment', 'max_size'))
[153]1265                status   = ''
[236]1266               
1267                for part in message_parts:
1268                        # Skip body parts
1269                        if not isinstance(part, tuple):
[22]1270                                continue
[236]1271                               
1272                        (original, filename, part) = part
[48]1273                        #
[172]1274                        # Must be tuneables HvB
1275                        #
[236]1276                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
[22]1277                        text = part.get_payload(decode=1)
1278                        if not text:
1279                                text = '(None)'
[48]1280                        fd.write(text)
1281                        fd.close()
[22]1282
[153]1283                        # get the file_size
[22]1284                        #
[48]1285                        stats = os.lstat(path)
[153]1286                        file_size = stats[stat.ST_SIZE]
[22]1287
[152]1288                        # Check if the attachment size is allowed
1289                        #
[153]1290                        if (max_size != -1) and (file_size > max_size):
1291                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
[236]1292                                        %(status, original, file_size, max_size)
[152]1293
1294                                os.unlink(path)
1295                                continue
1296                        else:
1297                                count = count + 1
1298                                       
[172]1299                        # Insert the attachment
[73]1300                        #
[242]1301                        fd = open(path, 'rb')
[250]1302                        att = attachment.Attachment(self.env, 'ticket', tkt_id)
[73]1303
[172]1304                        # This will break the ticket_update system, the body_text is vaporized
1305                        # ;-(
1306                        #
1307                        if not update:
1308                                att.author = self.author
1309                                att.description = self.email_to_unicode('Added by email2trac')
[73]1310
[236]1311                        att.insert(filename, fd, file_size)
[172]1312                        #except  util.TracError, detail:
1313                        #       print detail
[73]1314
[103]1315                        # Remove the created temporary filename
1316                        #
[172]1317                        fd.close()
[103]1318                        os.unlink(path)
1319
[77]1320                # Return how many attachments
1321                #
[153]1322                status = 'This message has %d attachment(s)\n%s' %(count, status)
1323                return status
[22]1324
[77]1325
[22]1326def mkdir_p(dir, mode):
1327        '''do a mkdir -p'''
1328
1329        arr = string.split(dir, '/')
1330        path = ''
1331        for part in arr:
1332                path = '%s/%s' % (path, part)
1333                try:
1334                        stats = os.stat(path)
1335                except OSError:
1336                        os.mkdir(path, mode)
1337
1338def ReadConfig(file, name):
1339        """
1340        Parse the config file
1341        """
1342        if not os.path.isfile(file):
[79]1343                print 'File %s does not exist' %file
[22]1344                sys.exit(1)
1345
[199]1346        config = trac_config.Configuration(file)
[22]1347
1348        # Use given project name else use defaults
1349        #
1350        if name:
[199]1351                sections = config.sections()
1352                if not name in sections:
[79]1353                        print "Not a valid project name: %s" %name
[199]1354                        print "Valid names: %s" %sections
[22]1355                        sys.exit(1)
1356
1357                project =  dict()
[199]1358                for option, value in  config.options(name):
1359                        project[option] = value
[22]1360
1361        else:
[217]1362                # use some trac internales to get the defaults
1363                #
1364                project = config.parser.defaults()
[22]1365
1366        return project
1367
[87]1368
[22]1369if __name__ == '__main__':
1370        # Default config file
1371        #
[24]1372        configfile = '@email2trac_conf@'
[22]1373        project = ''
1374        component = ''
[202]1375        ticket_prefix = 'default'
[204]1376        dry_run = None
[202]1377
[87]1378        ENABLE_SYSLOG = 0
[201]1379
[204]1380
[202]1381        SHORT_OPT = 'chf:np:t:'
1382        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
[201]1383
[22]1384        try:
[201]1385                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1386        except getopt.error,detail:
1387                print __doc__
1388                print detail
1389                sys.exit(1)
[87]1390       
[22]1391        project_name = None
1392        for opt,value in opts:
1393                if opt in [ '-h', '--help']:
1394                        print __doc__
1395                        sys.exit(0)
1396                elif opt in ['-c', '--component']:
1397                        component = value
1398                elif opt in ['-f', '--file']:
1399                        configfile = value
[201]1400                elif opt in ['-n', '--dry-run']:
[204]1401                        dry_run = True
[22]1402                elif opt in ['-p', '--project']:
1403                        project_name = value
[202]1404                elif opt in ['-t', '--ticket_prefix']:
1405                        ticket_prefix = value
[87]1406       
[22]1407        settings = ReadConfig(configfile, project_name)
1408        if not settings.has_key('project'):
1409                print __doc__
[79]1410                print 'No Trac project is defined in the email2trac config file.'
[22]1411                sys.exit(1)
[87]1412       
[22]1413        if component:
1414                settings['component'] = component
[202]1415
1416        # The default prefix for ticket values in email2trac.conf
1417        #
1418        settings['ticket_prefix'] = ticket_prefix
[206]1419        settings['dry_run'] = dry_run
[87]1420       
[22]1421        if settings.has_key('trac_version'):
[189]1422                version = settings['trac_version']
[22]1423        else:
1424                version = trac_default_version
1425
[189]1426
[22]1427        #debug HvB
1428        #print settings
[189]1429
[87]1430        try:
[189]1431                if version == '0.9':
[87]1432                        from trac import attachment
1433                        from trac.env import Environment
1434                        from trac.ticket import Ticket
1435                        from trac.web.href import Href
1436                        from trac import util
1437                        from trac.Notify import TicketNotifyEmail
[189]1438                elif version == '0.10':
[87]1439                        from trac import attachment
1440                        from trac.env import Environment
1441                        from trac.ticket import Ticket
1442                        from trac.web.href import Href
1443                        from trac import util
[139]1444                        #
1445                        # return  util.text.to_unicode(str)
1446                        #
[87]1447                        # see http://projects.edgewall.com/trac/changeset/2799
1448                        from trac.ticket.notification import TicketNotifyEmail
[199]1449                        from trac import config as trac_config
[189]1450                elif version == '0.11':
[182]1451                        from trac import attachment
1452                        from trac.env import Environment
1453                        from trac.ticket import Ticket
1454                        from trac.web.href import Href
[199]1455                        from trac import config as trac_config
[182]1456                        from trac import util
1457                        #
1458                        # return  util.text.to_unicode(str)
1459                        #
1460                        # see http://projects.edgewall.com/trac/changeset/2799
1461                        from trac.ticket.notification import TicketNotifyEmail
[189]1462                else:
1463                        print 'TRAC version %s is not supported' %version
1464                        sys.exit(1)
1465                       
1466                if settings.has_key('enable_syslog'):
[190]1467                        if SYSLOG_AVAILABLE:
1468                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1469
[87]1470                env = Environment(settings['project'], create=0)
[206]1471                tktparser = TicketEmailParser(env, settings, float(version))
[87]1472                tktparser.parse(sys.stdin)
[22]1473
[87]1474        # Catch all errors ans log to SYSLOG if we have enabled this
1475        # else stdout
1476        #
1477        except Exception, error:
1478                if ENABLE_SYSLOG:
1479                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1480
[87]1481                        etype, evalue, etb = sys.exc_info()
1482                        for e in traceback.format_exception(etype, evalue, etb):
1483                                syslog.syslog(e)
[187]1484
[87]1485                        syslog.closelog()
1486                else:
1487                        traceback.print_exc()
[22]1488
[97]1489                if m:
[98]1490                        tktparser.save_email_for_debug(m, True)
[97]1491
[249]1492                sys.exit(1)
[22]1493# EOB
Note: See TracBrowser for help on using the repository browser.