source: trunk/email2trac.py.in @ 239

Last change on this file since 239 was 239, checked in by bromine, 15 years ago

email2trac.py.in:

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 38.3 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 239 2008-12-18 21:48:25Z bromine $
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 :
329                                print '%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
[43]436        def set_owner(self, ticket):
[45]437                """
438                Select default owner for ticket component
439                """
[176]440                #### return self.get_config('ticket', 'default_component')
[43]441                cursor = self.db.cursor()
442                sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
443                cursor.execute(sql)
[61]444                try:
445                        ticket['owner'] = cursor.fetchone()[0]
446                except TypeError, detail:
[176]447                        ticket['owner'] = None
[43]448
[194]449        def get_sender_info(self, message):
[45]450                """
[72]451                Get the default author name and email address from the message
[226]452                """
[43]453
[226]454                self.email_to = self.email_to_unicode(message['to'])
455                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
456
[194]457                self.email_from = self.email_to_unicode(message['from'])
[223]458                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]459
[194]460                # Maybe for later user
461                #self.email_from =  self.email_to_unicode(self.email_addr)
462
463
464                if self.IGNORE_TRAC_USER_SETTINGS:
465                        return
466
467                # Is this a registered user, use email address as search key:
468                # result:
469                #   u : login name
470                #   n : Name that the user has set in the settings tab
471                #   e : email address that the user has set in the settings tab
[45]472                #
[194]473                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[72]474                                if e == self.email_addr ]
[43]475
[45]476                if len(users) == 1:
[194]477                        self.email_from = users[0][0]
[45]478
[72]479        def set_reply_fields(self, ticket, message):
480                """
481                Set all the right fields for a new ticket
482                """
[183]483                ticket['reporter'] = self.email_from
[72]484
[45]485                # Put all CC-addresses in ticket CC field
[43]486                #
487                if self.REPLY_ALL:
[45]488                        #tos = message.get_all('to', [])
[43]489                        ccs = message.get_all('cc', [])
490
[45]491                        addrs = email.Utils.getaddresses(ccs)
[105]492                        if not addrs:
493                                return
[43]494
495                        # Remove reporter email address if notification is
496                        # on
497                        #
498                        if self.notification:
499                                try:
[72]500                                        addrs.remove((self.author, self.email_addr))
[43]501                                except ValueError, detail:
502                                        pass
503
[45]504                        for name,mail in addrs:
[105]505                                try:
[108]506                                        mail_list = '%s, %s' %(mail_list, mail)
[230]507                                except UnboundLocalError, detail:
[105]508                                        mail_list = mail
[43]509
[105]510                        if mail_list:
[139]511                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]512
513        def save_email_for_debug(self, message, tempfile=False):
514                if tempfile:
515                        import tempfile
516                        msg_file = tempfile.mktemp('.email2trac')
517                else:
[173]518                        #msg_file = '/var/tmp/msg.txt'
519                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
520
[44]521                print 'TD: saving email to %s' % msg_file
522                fx = open(msg_file, 'wb')
523                fx.write('%s' % message)
524                fx.close()
525                try:
526                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
527                except OSError:
528                        pass
529
[167]530        def str_to_dict(self, str):
[164]531                """
532                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
533                """
534                # Skip the last ':' character
535                #
536                fields = string.split(str[:-1], ',')
537
538                result = dict()
539                for field in fields:
540                        try:
541                                index, value = string.split(field,'=')
[169]542
543                                # We can not change the description of a ticket via the subject
544                                # line. The description is the body of the email
545                                #
546                                if index.lower() in ['description']:
547                                        continue
548
[164]549                                if value:
[165]550                                        result[index.lower()] = value
[169]551
[164]552                        except ValueError:
553                                pass
554
[165]555                return result
[167]556
[202]557        def update_ticket_fields(self, ticket, user_dict, use_default=None):
558                """
559                This will update the ticket fields. It will check if the
560                given fields are known and if the right values are specified
561                It will only update the ticket field value:
[169]562                        - If the field is known
[202]563                        - If the value supplied is valid for the ticket field.
564                          If not then there are two options:
565                           1) Skip the value (use_default=None)
566                           2) Set default value for field (use_default=1)
[169]567                """
568
569                # Build a system dictionary from the ticket fields
570                # with field as index and option as value
571                #
572                sys_dict = dict()
573                for field in ticket.fields:
[167]574                        try:
[169]575                                sys_dict[field['name']] = field['options']
576
[167]577                        except KeyError:
[169]578                                sys_dict[field['name']] = None
[167]579                                pass
[169]580
581                # Check user supplied fields an compare them with the
582                # system one's
583                #
584                for field,value in user_dict.items():
[202]585                        if self.DEBUG >= 10:
586                                print  'user_field\t %s = %s' %(field,value)
[169]587
588                        if sys_dict.has_key(field):
589
590                                # Check if value is an allowed system option, if TypeError then
591                                # every value is allowed
592                                #
593                                try:
594                                        if value in sys_dict[field]:
595                                                ticket[field] = value
[202]596                                        else:
597                                                # Must we set a default if value is not allowed
598                                                #
599                                                if use_default:
600                                                        value = self.get_config('ticket', 'default_%s' %(field) )
601                                                        ticket[field] = value
[169]602
603                                except TypeError:
604                                        ticket[field] = value
[202]605
606                                if self.DEBUG >= 10:
607                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]608                                       
[204]609        def ticket_update(self, m, spam):
[78]610                """
[79]611                If the current email is a reply to an existing ticket, this function
612                will append the contents of this email to that ticket, instead of
613                creating a new one.
[78]614                """
[202]615
[71]616                if not m['Subject']:
617                        return False
618                else:
[139]619                        subject  = self.email_to_unicode(m['Subject'])
[71]620
[182]621                # [hic] #1529: Re: LRZ
622                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
623                #
[71]624                TICKET_RE = re.compile(r"""
625                                        (?P<ticketnr>[#][0-9]+:)
[194]626                                        |(?P<ticketnr_fields>[#][\d]+\?.*?:)
[71]627                                        """, re.VERBOSE)
628
629                result =  TICKET_RE.search(subject)
630                if not result:
631                        return False
632
[164]633                # Must we update ticket fields
634                #
[220]635                update_fields = dict()
[165]636                try:
[164]637                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
[220]638                        update_fields = self.str_to_dict(keywords)
[165]639
640                        # Strip '#'
641                        #
642                        ticket_id = int(nr[1:])
643
644                except AttributeError:
645                        # Strip '#' and ':'
646                        #
[167]647                        nr = result.group('ticketnr')
[165]648                        ticket_id = int(nr[1:-1])
[164]649
[71]650
[194]651                # When is the change committed
652                #
[77]653                #
[194]654                if self.VERSION == 0.11:
655                        utc = UTC()
656                        when = datetime.now(utc)
657                else:
658                        when = int(time.time())
[77]659
[172]660                try:
661                        tkt = Ticket(self.env, ticket_id, self.db)
662                except util.TracError, detail:
663                        return False
[126]664
[220]665                # reopen the ticket if it is was closed
666                # We must use the ticket workflow framework
667                #
668                if tkt['status'] in ['closed']:
669                        tkt['status'] = 'reopened'
670                        tkt['resolution'] = ''
671
[172]672                # Must we update some ticket fields properties
673                #
[220]674                if update_fields:
675                        self.update_ticket_fields(tkt, update_fields)
[166]676
[236]677                message_parts = self.get_message_parts(m)
678                message_parts = self.unique_attachment_names(message_parts, tkt)
[210]679
[177]680                if self.EMAIL_HEADER:
[236]681                        message_parts.insert(0, self.email_header_txt(m))
[76]682
[236]683                body_text = self.body_text(message_parts)
684
[219]685                if body_text.strip():
686                        tkt.save_changes(self.author, body_text, when)
687
[172]688                tkt['id'] = ticket_id
689
[129]690                if self.VERSION  == 0.9:
[236]691                        str = self.attachments(message_parts, tkt, True)
[129]692                else:
[236]693                        str = self.attachments(message_parts, tkt)
[76]694
[204]695                if self.notification and not spam:
[77]696                        self.notify(tkt, False, when)
[72]697
[71]698                return True
699
[202]700        def set_ticket_fields(self, ticket):
[77]701                """
[202]702                set the ticket fields to value specified
703                        - /etc/email2trac.conf with <prefix>_<field>
704                        - trac default values, trac.ini
705                """
706                user_dict = dict()
707
708                for field in ticket.fields:
709
710                        name = field['name']
711
[215]712                        # skip some fields like resolution
713                        #
714                        if name in [ 'resolution' ]:
715                                continue
716
[202]717                        # default trac value
718                        #
[233]719                        if not field.get('custom'):
720                                value = self.get_config('ticket', 'default_%s' %(name) )
721                        else:
722                                value = field.get('value')
723                                options = field.get('options')
[234]724                                if value and options and value not in options:
[233]725                                        value = options[int(value)]
726
[202]727                        if self.DEBUG > 10:
728                                print 'trac.ini name %s = %s' %(name, value)
729
[206]730                        prefix = self.parameters['ticket_prefix']
[202]731                        try:
[206]732                                value = self.parameters['%s_%s' %(prefix, name)]
[202]733                                if self.DEBUG > 10:
734                                        print 'email2trac.conf %s = %s ' %(name, value)
735
736                        except KeyError, detail:
737                                pass
738               
739                        if self.DEBUG:
740                                print 'user_dict[%s] = %s' %(name, value)
741
742                        user_dict[name] = value
743
744                self.update_ticket_fields(ticket, user_dict, use_default=1)
745
746                # Set status ticket
747                #`
748                ticket['status'] = 'new'
749
750
751
[204]752        def new_ticket(self, msg, spam):
[202]753                """
[77]754                Create a new ticket
755                """
[41]756                tkt = Ticket(self.env)
[22]757
[202]758                self.set_ticket_fields(tkt)
759
[22]760                # Some defaults
761                #
[202]762                #tkt['status'] = 'new'
763                #tkt['milestone'] = self.get_config('ticket', 'default_milestone')
764                #tkt['priority'] = self.get_config('ticket', 'default_priority')
765                #tkt['severity'] = self.get_config('ticket', 'default_severity')
766                #tkt['version'] = self.get_config('ticket', 'default_version')
767                #tkt['type'] = self.get_config('ticket', 'default_type')
[22]768
[202]769                # Old style setting for component, will be removed
770                #
[204]771                if spam:
772                        tkt['component'] = 'Spam'
773
[206]774                elif self.parameters.has_key('component'):
775                        tkt['component'] = self.parameters['component']
[201]776
[22]777                if not msg['Subject']:
[151]778                        tkt['summary'] = u'(No subject)'
[22]779                else:
[139]780                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]781
782
[72]783                self.set_reply_fields(tkt, msg)
[22]784
[45]785                # produce e-mail like header
786                #
[22]787                head = ''
788                if self.EMAIL_HEADER > 0:
789                        head = self.email_header_txt(msg)
[92]790                       
[236]791                message_parts = self.get_message_parts(msg)
792                message_parts = self.unique_attachment_names(message_parts)
793               
794                if self.EMAIL_HEADER > 0:
795                        message_parts.insert(0, self.email_header_txt(msg))
796                       
797                body_text = self.body_text(message_parts)
[45]798
[236]799                tkt['description'] = body_text
[90]800
[182]801                #when = int(time.time())
[192]802                #
[182]803                utc = UTC()
804                when = datetime.now(utc)
[45]805
[204]806                if self.DRY_RUN:
[201]807                        ticket_id = 'DRY_RUN'
808                else:
809                        ticket_id = tkt.insert()
[187]810                       
[172]811                tkt['id'] = ticket_id
812
[90]813                changed = False
814                comment = ''
[77]815
[90]816                # Rewrite the description if we have mailto enabled
[45]817                #
[72]818                if self.MAILTO:
[100]819                        changed = True
[142]820                        comment = u'\nadded mailto line\n'
[210]821                        #mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
822                        mailto = self.html_mailto_link( m['Subject'], ticket_id, body_text)
[213]823                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]824                                %(head, mailto, body_text)
[45]825
[236]826                str =  self.attachments(message_parts, tkt)
[152]827                if str:
[100]828                        changed = True
[152]829                        comment = '%s\n%s\n' %(comment, str)
[77]830
[90]831                if changed:
[204]832                        if self.DRY_RUN:
[201]833                                print 'DRY_RUN: tkt.save_changes(self.author, comment)'
834                        else:
835                                tkt.save_changes(self.author, comment)
836                                #print tkt.get_changelog(self.db, when)
[90]837
[45]838                if self.notification:
[204]839                        if self.DRY_RUN:
[202]840                                print 'DRY_RUN: self.notify(tkt, True)'
841                        else:
[204]842                                if not spam:
843                                        self.notify(tkt, True)
[202]844                                #self.notify(tkt, False)
[45]845
[77]846        def parse(self, fp):
[96]847                global m
848
[77]849                m = email.message_from_file(fp)
[239]850               
[77]851                if not m:
[221]852                        if self.DEBUG:
853                                print "This is not a valid email message format"
[77]854                        return
[239]855                       
856                print repr(m['Subject'])
[77]857
[239]858                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
859                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
860
[77]861                if self.DEBUG > 1:        # save the entire e-mail message text
[236]862                        message_parts = self.get_message_parts(m)
863                        message_parts = self.unique_attachment_names(message_parts)
[219]864                        self.save_email_for_debug(m, True)
[236]865                        body_text = self.body_text(message_parts)
866                        self.debug_body(body_text, True)
867                        self.debug_attachments(message_parts)
[77]868
869                self.db = self.env.get_db_cnx()
[194]870                self.get_sender_info(m)
[152]871
[221]872                if not self.email_header_acl('white_list', self.email_addr, True):
873                        if self.DEBUG > 1 :
874                                print 'Message rejected : %s not in white list' %(self.email_addr)
875                        return False
[77]876
[221]877                if self.email_header_acl('black_list', self.email_addr, False):
878                        if self.DEBUG > 1 :
879                                print 'Message rejected : %s in black list' %(self.email_addr)
880                        return False
881
[227]882                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]883                        if self.DEBUG > 1 :
884                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
885                        return False
886
[204]887                # If drop the message
[194]888                #
[204]889                if self.spam(m) == 'drop':
[194]890                        return False
891
[204]892                elif self.spam(m) == 'spam':
893                        spam_msg = True
[194]894
[204]895                else:
896                        spam_msg = False
897
[77]898                if self.get_config('notification', 'smtp_enabled') in ['true']:
899                        self.notification = 1
900                else:
901                        self.notification = 0
902
903                # Must we update existing tickets
904                #
905                if self.TICKET_UPDATE > 0:
[204]906                        if self.ticket_update(m, spam_msg):
[77]907                                return True
908
[204]909                self.new_ticket(m, spam_msg)
[77]910
[136]911        def strip_signature(self, text):
912                """
913                Strip signature from message, inspired by Mailman software
914                """
915                body = []
916                for line in text.splitlines():
917                        if line == '-- ':
918                                break
919                        body.append(line)
920
921                return ('\n'.join(body))
922
[231]923        def reflow(self, text, delsp = 0):
924                """
925                Reflow the message based on the format="flowed" specification (RFC 3676)
926                """
927                flowedlines = []
928                quotelevel = 0
929                prevflowed = 0
930
931                for line in text.splitlines():
932                        from re import match
933                       
934                        # Figure out the quote level and the content of the current line
935                        m = match('(>*)( ?)(.*)', line)
936                        linequotelevel = len(m.group(1))
937                        line = m.group(3)
938
939                        # Determine whether this line is flowed
940                        if line and line != '-- ' and line[-1] == ' ':
941                                flowed = 1
942                        else:
943                                flowed = 0
944
945                        if flowed and delsp and line and line[-1] == ' ':
946                                line = line[:-1]
947
948                        # If the previous line is flowed, append this line to it
949                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
950                                flowedlines[-1] += line
951                        # Otherwise, start a new line
952                        else:
953                                flowedlines.append('>' * linequotelevel + line)
954
955                        prevflowed = flowed
956                       
957
958                return '\n'.join(flowedlines)
959
[191]960        def strip_quotes(self, text):
[193]961                """
962                Strip quotes from message by Nicolas Mendoza
963                """
964                body = []
965                for line in text.splitlines():
966                        if line.startswith(self.EMAIL_QUOTE):
967                                continue
968                        body.append(line)
[151]969
[193]970                return ('\n'.join(body))
[191]971
[154]972        def wrap_text(self, text, replace_whitespace = False):
[151]973                """
[191]974                Will break a lines longer then given length into several small
975                lines of size given length
[151]976                """
977                import textwrap
[154]978
[151]979                LINESEPARATOR = '\n'
[153]980                reformat = ''
[151]981
[154]982                for s in text.split(LINESEPARATOR):
983                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
984                        if tmp:
985                                reformat = '%s\n%s' %(reformat,tmp)
986                        else:
987                                reformat = '%s\n' %reformat
[153]988
989                return reformat
990
[154]991                # Python2.4 and higher
992                #
993                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
994                #
995
996
[236]997        def get_message_parts(self, msg):
[45]998                """
[236]999                parses the email message and returns a list of body parts and attachments
1000                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1001                """
[236]1002                message_parts = []
[238]1003               
1004                # This is used to figure out when we are inside an AppleDouble container
1005                # AppleDouble containers consists of two parts: Mac-specific file data, and platform-independent data
1006                # We strip away Mac-specific stuff
1007                appledouble_parts = []
[236]1008
[22]1009                for part in msg.walk():
[236]1010                        if self.DEBUG:
1011                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[238]1012                               
1013                        # Check whether we just finished processing an AppleDouble container
1014                        if part not in appledouble_parts:
1015                                appledouble_parts = []
[236]1016
[238]1017                        # Special handling for BinHex attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1018                        if part.get_content_type() == 'application/mac-binhex40':
1019                                if self.BINHEX == 'warn':
1020                                        message_parts.append("'''A BinHex attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1021                                        continue
1022                                elif self.BINHEX == 'drop':
1023                                        continue
1024
1025                        # Special handling for AppleSingle attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1026                        if part.get_content_type() == 'application/applefile' and not part in appledouble_parts:
1027                                if self.APPLESINGLE == 'warn':
1028                                        message_parts.append("'''An AppleSingle attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1029                                        continue
1030                                elif self.APPLESINGLE == 'drop':
1031                                        continue
1032
1033                        # Special handling for the Mac-specific part of AppleDouble attachments. Options are strip (leave out with no warning), warn (and leave out), and keep
1034                        if part.get_content_type() == 'application/applefile':
1035                                if self.APPLEDOUBLE == 'warn':
1036                                        message_parts.append("'''The resource fork of an attachment named '%s' was removed.'''" % part.get_filename())
1037                                        continue
1038                                elif self.APPLEDOUBLE == 'strip':
1039                                        continue
1040
1041                        # If we entering an AppleDouble container, set up appledouble_parts so that we know what to do with its subparts
1042                        if part.get_content_type() == 'multipart/appledouble':
1043                                appledouble_parts = part.get_payload()
1044                                continue
1045
1046                        # Any other multipart/* is just a container for multipart messages
[45]1047                        if part.get_content_maintype() == 'multipart':
[22]1048                                continue
1049
[236]1050                        # 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"
1051                        inline = self.inline_part(part)
1052
1053                        # Inline text parts are where the body is
1054                        if part.get_content_type() == 'text/plain' and inline:
1055                                if self.DEBUG:
1056                                        print 'TD:               Inline body part'
1057
[45]1058                                # Try to decode, if fails then do not decode
1059                                #
[90]1060                                body_text = part.get_payload(decode=1)
[45]1061                                if not body_text:                       
[90]1062                                        body_text = part.get_payload(decode=0)
[231]1063
[232]1064                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1065                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1066
1067                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1068                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1069       
[136]1070                                if self.STRIP_SIGNATURE:
1071                                        body_text = self.strip_signature(body_text)
[22]1072
[191]1073                                if self.STRIP_QUOTES:
1074                                        body_text = self.strip_quotes(body_text)
1075
[148]1076                                if self.USE_TEXTWRAP:
[151]1077                                        body_text = self.wrap_text(body_text)
[148]1078
[45]1079                                # Get contents charset (iso-8859-15 if not defined in mail headers)
1080                                #
[100]1081                                charset = part.get_content_charset()
[102]1082                                if not charset:
1083                                        charset = 'iso-8859-15'
1084
[89]1085                                try:
[96]1086                                        ubody_text = unicode(body_text, charset)
[100]1087
1088                                except UnicodeError, detail:
[96]1089                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1090
[100]1091                                except LookupError, detail:
[139]1092                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1093
[236]1094                                if self.VERBATIM_FORMAT:
1095                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1096                                else:
1097                                        message_parts.append('%s' %ubody_text)
1098                        else:
1099                                if self.DEBUG:
1100                                        print 'TD:               Filename: %s' % part.get_filename()
[22]1101
[236]1102                                message_parts.append((part.get_filename(), part))
1103
1104                return message_parts
1105               
1106        def unique_attachment_names(self, message_parts, tkt = None):
1107                renamed_parts = []
1108                attachment_names = set()
1109                for part in message_parts:
1110                       
1111                        # If not an attachment, leave it alone
1112                        if not isinstance(part, tuple):
1113                                renamed_parts.append(part)
1114                                continue
1115                               
1116                        (filename, part) = part
1117                        # Decode the filename
1118                        if filename:
1119                                filename = self.email_to_unicode(filename)                     
1120                        # If no name, use a default one
[22]1121                        else:
[236]1122                                filename = 'untitled-part'
[22]1123
[236]1124                                # Guess the extension from the content type
1125                                ext = mimetypes.guess_extension(part.get_content_type())
1126                                if not ext:
1127                                        ext = '.bin'
[22]1128
[236]1129                                filename = '%s%s' % (filename, ext)
[22]1130
[236]1131                        # Discard relative paths in attachment names
1132                        filename = filename.replace('\\', '/').replace(':', '/')
1133                        filename = os.path.basename(filename)
[22]1134
[236]1135                        # We try to normalize the filename to utf-8 NFC if we can.
1136                        # Files uploaded from OS X might be in NFD.
1137                        # Check python version and then try it
1138                        #
1139                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1140                                try:
1141                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1142                                except TypeError:
1143                                        pass
1144                                       
1145                        if self.QUOTE_ATTACHMENT_FILENAMES:
1146                                filename = urllib.quote(filename)
[100]1147
[236]1148                        # Make the filename unique for this ticket
1149                        num = 0
1150                        unique_filename = filename
1151                        filename, ext = os.path.splitext(filename)
[134]1152
[236]1153                        while unique_filename in attachment_names or self.attachment_exists(tkt, unique_filename):
1154                                num += 1
1155                                unique_filename = "%s-%s%s" % (filename, num, ext)
1156                               
1157                        if self.DEBUG:
1158                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[100]1159
[236]1160                        attachment_names.add(unique_filename)
1161
1162                        renamed_parts.append((filename, unique_filename, part))
1163               
1164                return renamed_parts
1165                       
1166        def inline_part(self, part):
1167                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1168               
1169                       
1170        def attachment_exists(self, tkt, filename):
1171                if tkt is None:
1172                        return False
1173                       
1174                try:
1175                        Attachment(self.env, 'ticket', tkt['id'], filename)
1176                        return True
1177                except ResourceNotFound:
1178                        return False
1179                       
1180        def body_text(self, message_parts):
1181                body_text = []
1182               
1183                for part in message_parts:
1184                        # Plain text part, append it
1185                        if not isinstance(part, tuple):
1186                                body_text.extend(part.strip().splitlines())
1187                                body_text.append("")
1188                                continue
1189                               
1190                        (original, filename, part) = part
1191                        inline = self.inline_part(part)
1192                       
1193                        if part.get_content_maintype() == 'image' and inline:
1194                                body_text.append('[[Image(%s)]]' % filename)
1195                                body_text.append("")
1196                        else:
1197                                body_text.append('[attachment:"%s"]' % filename)
1198                                body_text.append("")
1199                               
1200                body_text = '\r\n'.join(body_text)
1201                return body_text
1202
[77]1203        def notify(self, tkt , new=True, modtime=0):
[79]1204                """
1205                A wrapper for the TRAC notify function. So we can use templates
1206                """
[41]1207                try:
1208                        # create false {abs_}href properties, to trick Notify()
1209                        #
[193]1210                        if not self.VERSION == 0.11:
[192]1211                                self.env.abs_href = Href(self.get_config('project', 'url'))
1212                                self.env.href = Href(self.get_config('project', 'url'))
[22]1213
[41]1214                        tn = TicketNotifyEmail(self.env)
[213]1215
[42]1216                        if self.notify_template:
[222]1217
[221]1218                                if self.VERSION == 0.11:
[222]1219
[221]1220                                        from trac.web.chrome import Chrome
[222]1221
1222                                        if self.notify_template_update and not new:
1223                                                tn.template_name = self.notify_template_update
1224                                        else:
1225                                                tn.template_name = self.notify_template
1226
[221]1227                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1228                                               
1229                                else:
[222]1230
[221]1231                                        tn.template_name = self.notify_template;
[42]1232
[77]1233                        tn.notify(tkt, new, modtime)
[41]1234
1235                except Exception, e:
[79]1236                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
[41]1237
[72]1238        def html_mailto_link(self, subject, id, body):
1239                if not self.author:
[143]1240                        author = self.email_addr
[22]1241                else:   
[142]1242                        author = self.author
[22]1243
1244                # Must find a fix
1245                #
1246                #arr = string.split(body, '\n')
1247                #arr = map(self.mail_line, arr)
1248                #body = string.join(arr, '\n')
1249                #body = '%s wrote:\n%s' %(author, body)
1250
1251                # Temporary fix
[142]1252                #
[74]1253                str = 'mailto:%s?Subject=%s&Cc=%s' %(
1254                       urllib.quote(self.email_addr),
1255                           urllib.quote('Re: #%s: %s' %(id, subject)),
1256                           urllib.quote(self.MAILTO_CC)
1257                           )
1258
[213]1259                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]1260                return str
1261
[236]1262        def attachments(self, message_parts, ticket, update=False):
[79]1263                '''
1264                save any attachments as files in the ticket's directory
1265                '''
[237]1266                if self.DRY_RUN:
1267                        return ''
1268
[22]1269                count = 0
[77]1270                first = 0
1271                number = 0
[152]1272
1273                # Get Maxium attachment size
1274                #
1275                max_size = int(self.get_config('attachment', 'max_size'))
[153]1276                status   = ''
[236]1277               
1278                for part in message_parts:
1279                        # Skip body parts
1280                        if not isinstance(part, tuple):
[22]1281                                continue
[236]1282                               
1283                        (original, filename, part) = part
[48]1284                        #
[172]1285                        # Must be tuneables HvB
1286                        #
[236]1287                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
[22]1288                        text = part.get_payload(decode=1)
1289                        if not text:
1290                                text = '(None)'
[48]1291                        fd.write(text)
1292                        fd.close()
[22]1293
[153]1294                        # get the file_size
[22]1295                        #
[48]1296                        stats = os.lstat(path)
[153]1297                        file_size = stats[stat.ST_SIZE]
[22]1298
[152]1299                        # Check if the attachment size is allowed
1300                        #
[153]1301                        if (max_size != -1) and (file_size > max_size):
1302                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
[236]1303                                        %(status, original, file_size, max_size)
[152]1304
1305                                os.unlink(path)
1306                                continue
1307                        else:
1308                                count = count + 1
1309                                       
[172]1310                        # Insert the attachment
[73]1311                        #
[172]1312                        fd = open(path)
1313                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
[73]1314
[172]1315                        # This will break the ticket_update system, the body_text is vaporized
1316                        # ;-(
1317                        #
1318                        if not update:
1319                                att.author = self.author
1320                                att.description = self.email_to_unicode('Added by email2trac')
[73]1321
[236]1322                        att.insert(filename, fd, file_size)
[172]1323                        #except  util.TracError, detail:
1324                        #       print detail
[73]1325
[103]1326                        # Remove the created temporary filename
1327                        #
[172]1328                        fd.close()
[103]1329                        os.unlink(path)
1330
[77]1331                # Return how many attachments
1332                #
[153]1333                status = 'This message has %d attachment(s)\n%s' %(count, status)
1334                return status
[22]1335
[77]1336
[22]1337def mkdir_p(dir, mode):
1338        '''do a mkdir -p'''
1339
1340        arr = string.split(dir, '/')
1341        path = ''
1342        for part in arr:
1343                path = '%s/%s' % (path, part)
1344                try:
1345                        stats = os.stat(path)
1346                except OSError:
1347                        os.mkdir(path, mode)
1348
1349def ReadConfig(file, name):
1350        """
1351        Parse the config file
1352        """
1353        if not os.path.isfile(file):
[79]1354                print 'File %s does not exist' %file
[22]1355                sys.exit(1)
1356
[199]1357        config = trac_config.Configuration(file)
[22]1358
1359        # Use given project name else use defaults
1360        #
1361        if name:
[199]1362                sections = config.sections()
1363                if not name in sections:
[79]1364                        print "Not a valid project name: %s" %name
[199]1365                        print "Valid names: %s" %sections
[22]1366                        sys.exit(1)
1367
1368                project =  dict()
[199]1369                for option, value in  config.options(name):
1370                        project[option] = value
[22]1371
1372        else:
[217]1373                # use some trac internales to get the defaults
1374                #
1375                project = config.parser.defaults()
[22]1376
1377        return project
1378
[87]1379
[22]1380if __name__ == '__main__':
1381        # Default config file
1382        #
[24]1383        configfile = '@email2trac_conf@'
[22]1384        project = ''
1385        component = ''
[202]1386        ticket_prefix = 'default'
[204]1387        dry_run = None
[202]1388
[87]1389        ENABLE_SYSLOG = 0
[201]1390
[204]1391
[202]1392        SHORT_OPT = 'chf:np:t:'
1393        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
[201]1394
[22]1395        try:
[201]1396                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1397        except getopt.error,detail:
1398                print __doc__
1399                print detail
1400                sys.exit(1)
[87]1401       
[22]1402        project_name = None
1403        for opt,value in opts:
1404                if opt in [ '-h', '--help']:
1405                        print __doc__
1406                        sys.exit(0)
1407                elif opt in ['-c', '--component']:
1408                        component = value
1409                elif opt in ['-f', '--file']:
1410                        configfile = value
[201]1411                elif opt in ['-n', '--dry-run']:
[204]1412                        dry_run = True
[22]1413                elif opt in ['-p', '--project']:
1414                        project_name = value
[202]1415                elif opt in ['-t', '--ticket_prefix']:
1416                        ticket_prefix = value
[87]1417       
[22]1418        settings = ReadConfig(configfile, project_name)
1419        if not settings.has_key('project'):
1420                print __doc__
[79]1421                print 'No Trac project is defined in the email2trac config file.'
[22]1422                sys.exit(1)
[87]1423       
[22]1424        if component:
1425                settings['component'] = component
[202]1426
1427        # The default prefix for ticket values in email2trac.conf
1428        #
1429        settings['ticket_prefix'] = ticket_prefix
[206]1430        settings['dry_run'] = dry_run
[87]1431       
[22]1432        if settings.has_key('trac_version'):
[189]1433                version = settings['trac_version']
[22]1434        else:
1435                version = trac_default_version
1436
[189]1437
[22]1438        #debug HvB
1439        #print settings
[189]1440
[87]1441        try:
[189]1442                if version == '0.9':
[87]1443                        from trac import attachment
1444                        from trac.env import Environment
1445                        from trac.ticket import Ticket
1446                        from trac.web.href import Href
1447                        from trac import util
1448                        from trac.Notify import TicketNotifyEmail
[189]1449                elif version == '0.10':
[87]1450                        from trac import attachment
1451                        from trac.env import Environment
1452                        from trac.ticket import Ticket
1453                        from trac.web.href import Href
1454                        from trac import util
[139]1455                        #
1456                        # return  util.text.to_unicode(str)
1457                        #
[87]1458                        # see http://projects.edgewall.com/trac/changeset/2799
1459                        from trac.ticket.notification import TicketNotifyEmail
[199]1460                        from trac import config as trac_config
[189]1461                elif version == '0.11':
[182]1462                        from trac import attachment
1463                        from trac.env import Environment
1464                        from trac.ticket import Ticket
1465                        from trac.web.href import Href
[199]1466                        from trac import config as trac_config
[182]1467                        from trac import util
1468                        #
1469                        # return  util.text.to_unicode(str)
1470                        #
1471                        # see http://projects.edgewall.com/trac/changeset/2799
1472                        from trac.ticket.notification import TicketNotifyEmail
[189]1473                else:
1474                        print 'TRAC version %s is not supported' %version
1475                        sys.exit(1)
1476                       
1477                if settings.has_key('enable_syslog'):
[190]1478                        if SYSLOG_AVAILABLE:
1479                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1480
[87]1481                env = Environment(settings['project'], create=0)
[206]1482                tktparser = TicketEmailParser(env, settings, float(version))
[87]1483                tktparser.parse(sys.stdin)
[22]1484
[87]1485        # Catch all errors ans log to SYSLOG if we have enabled this
1486        # else stdout
1487        #
1488        except Exception, error:
1489                if ENABLE_SYSLOG:
1490                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1491
[87]1492                        etype, evalue, etb = sys.exc_info()
1493                        for e in traceback.format_exception(etype, evalue, etb):
1494                                syslog.syslog(e)
[187]1495
[87]1496                        syslog.closelog()
1497                else:
1498                        traceback.print_exc()
[22]1499
[97]1500                if m:
[98]1501                        tktparser.save_email_for_debug(m, True)
[97]1502
[22]1503# EOB
Note: See TracBrowser for help on using the repository browser.