source: trunk/email2trac.py.in @ 390

Last change on this file since 390 was 390, checked in by bas, 14 years ago

Fixed several errors for a ticket update

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 54.6 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
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
[22]44
[282]45    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
46    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]47       
48 * default config file is : /etc/email2trac.conf
49
50 * Commandline opions:
[205]51                -h,--help
52                -f,--file  <configuration file>
53                -n,--dry-run
54                -p, --project <project name>
55                -t, --ticket_prefix <name>
[22]56
57SVN Info:
58        $Id: email2trac.py.in 390 2010-07-06 12:02:55Z bas $
59"""
60import os
61import sys
62import string
63import getopt
64import stat
65import time
66import email
[136]67import email.Iterators
68import email.Header
[22]69import re
70import urllib
71import unicodedata
72from stat import *
73import mimetypes
[96]74import traceback
[22]75
[363]76from trac import __version__ as trac_version
[190]77
78# Will fail where unavailable, e.g. Windows
79#
80try:
81    import syslog
82    SYSLOG_AVAILABLE = True
83except ImportError:
84    SYSLOG_AVAILABLE = False
85
[182]86from datetime import tzinfo, timedelta, datetime
[199]87from trac import config as trac_config
[91]88
[96]89# Some global variables
90#
[276]91trac_default_version = '0.11'
[96]92m = None
[22]93
[182]94# A UTC class needed for trac version 0.11, added by
95# tbaschak at ktc dot mb dot ca
96#
97class UTC(tzinfo):
98        """UTC"""
99        ZERO = timedelta(0)
100        HOUR = timedelta(hours=1)
101       
102        def utcoffset(self, dt):
103                return self.ZERO
104               
105        def tzname(self, dt):
106                return "UTC"
107               
108        def dst(self, dt):
109                return self.ZERO
110
111
[22]112class TicketEmailParser(object):
113        env = None
114        comment = '> '
[359]115
[206]116        def __init__(self, env, parameters, version):
[22]117                self.env = env
118
119                # Database connection
120                #
121                self.db = None
122
[206]123                # Save parameters
124                #
125                self.parameters = parameters
126
[72]127                # Some useful mail constants
128                #
[287]129                self.email_name = None
[72]130                self.email_addr = None
[183]131                self.email_from = None
[288]132                self.author     = None
[287]133                self.id         = None
[294]134               
135                self.STRIP_CONTENT_TYPES = list()
[72]136
[22]137                self.VERSION = version
[206]138                self.DRY_RUN = parameters['dry_run']
[317]139                self.VERBOSE = parameters['verbose']
[204]140
[172]141                self.get_config = self.env.config.get
[22]142
143                if parameters.has_key('umask'):
144                        os.umask(int(parameters['umask'], 8))
145
146                if parameters.has_key('debug'):
147                        self.DEBUG = int(parameters['debug'])
148                else:
149                        self.DEBUG = 0
150
151                if parameters.has_key('mailto_link'):
152                        self.MAILTO = int(parameters['mailto_link'])
[74]153                        if parameters.has_key('mailto_cc'):
154                                self.MAILTO_CC = parameters['mailto_cc']
155                        else:
156                                self.MAILTO_CC = ''
[22]157                else:
158                        self.MAILTO = 0
159
160                if parameters.has_key('spam_level'):
161                        self.SPAM_LEVEL = int(parameters['spam_level'])
162                else:
163                        self.SPAM_LEVEL = 0
164
[207]165                if parameters.has_key('spam_header'):
166                        self.SPAM_HEADER = parameters['spam_header']
167                else:
168                        self.SPAM_HEADER = 'X-Spam-Score'
169
[191]170                if parameters.has_key('email_quote'):
171                        self.EMAIL_QUOTE = str(parameters['email_quote'])
172                else:   
173                        self.EMAIL_QUOTE = '> '
[22]174
175                if parameters.has_key('email_header'):
176                        self.EMAIL_HEADER = int(parameters['email_header'])
177                else:
178                        self.EMAIL_HEADER = 0
179
[42]180                if parameters.has_key('alternate_notify_template'):
181                        self.notify_template = str(parameters['alternate_notify_template'])
182                else:
183                        self.notify_template = None
[22]184
[222]185                if parameters.has_key('alternate_notify_template_update'):
186                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
187                else:
188                        self.notify_template_update = None
189
[43]190                if parameters.has_key('reply_all'):
191                        self.REPLY_ALL = int(parameters['reply_all'])
192                else:
193                        self.REPLY_ALL = 0
[42]194
[74]195                if parameters.has_key('ticket_update'):
196                        self.TICKET_UPDATE = int(parameters['ticket_update'])
197                else:
198                        self.TICKET_UPDATE = 0
[43]199
[353]200                if parameters.has_key('ticket_update_by_subject'):
201                        self.TICKET_UPDATE_BY_SUBJECT = int(parameters['ticket_update_by_subject'])
202                else:
203                        self.TICKET_UPDATE_BY_SUBJECT = 0
204
[360]205                if parameters.has_key('ticket_update_by_subject_lookback'):
206                        self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK = int(parameters['ticket_update_by_subject_lookback'])
207                else:
208                        self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK = 30
209
[118]210                if parameters.has_key('drop_spam'):
211                        self.DROP_SPAM = int(parameters['drop_spam'])
212                else:
213                        self.DROP_SPAM = 0
[74]214
[134]215                if parameters.has_key('verbatim_format'):
216                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
217                else:
218                        self.VERBATIM_FORMAT = 1
[118]219
[231]220                if parameters.has_key('reflow'):
[256]221                        self.REFLOW = int(parameters['reflow'])
[231]222                else:
223                        self.REFLOW = 1
224
[278]225                if parameters.has_key('drop_alternative_html_version'):
226                        self.DROP_ALTERNATIVE_HTML_VERSION = int(parameters['drop_alternative_html_version'])
227                else:
228                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
229
[136]230                if parameters.has_key('strip_signature'):
231                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
232                else:
233                        self.STRIP_SIGNATURE = 0
[134]234
[191]235                if parameters.has_key('strip_quotes'):
236                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
237                else:
238                        self.STRIP_QUOTES = 0
239
[309]240                self.properties = dict()
241                if parameters.has_key('inline_properties'):
242                        self.INLINE_PROPERTIES = int(parameters['inline_properties'])
243                else:
244                        self.INLINE_PROPERTIES = 0
245
[148]246                if parameters.has_key('use_textwrap'):
247                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
248                else:
249                        self.USE_TEXTWRAP = 0
250
[238]251                if parameters.has_key('binhex'):
[294]252                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]253
254                if parameters.has_key('applesingle'):
[294]255                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]256
257                if parameters.has_key('appledouble'):
[294]258                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]259
[294]260                if parameters.has_key('strip_content_types'):
261                        items = parameters['strip_content_types'].split(',')
262                        for item in items:
263                                self.STRIP_CONTENT_TYPES.append(item.strip())
264
[257]265                self.WORKFLOW = None
266                if parameters.has_key('workflow'):
267                        self.WORKFLOW = parameters['workflow']
268
[173]269                # Use OS independend functions
270                #
271                self.TMPDIR = os.path.normcase('/tmp')
272                if parameters.has_key('tmpdir'):
273                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
274
[194]275                if parameters.has_key('ignore_trac_user_settings'):
276                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
277                else:
278                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]279
[320]280                if parameters.has_key('email_triggers_workflow'):
281                        self.EMAIL_TRIGGERS_WORKFLOW = int(parameters['email_triggers_workflow'])
282                else:
283                        self.EMAIL_TRIGGERS_WORKFLOW = 1
284
[297]285                if parameters.has_key('subject_field_separator'):
286                        self.SUBJECT_FIELD_SEPARATOR = parameters['subject_field_separator'].strip()
287                else:
288                        self.SUBJECT_FIELD_SEPARATOR = '&'
289
[305]290                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
291
[359]292                self.system = None
293
[341]294########## Email Header Functions ###########################################################
[339]295
[22]296        def spam(self, message):
[191]297                """
298                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
299                # Note if Spam_level then '*' are included
300                """
[194]301                spam = False
[207]302                if message.has_key(self.SPAM_HEADER):
303                        spam_l = string.split(message[self.SPAM_HEADER])
[22]304
[207]305                        try:
306                                number = spam_l[0].count('*')
307                        except IndexError, detail:
308                                number = 0
309                               
[22]310                        if number >= self.SPAM_LEVEL:
[194]311                                spam = True
312                               
[191]313                # treat virus mails as spam
314                #
315                elif message.has_key('X-Virus-found'):                 
[194]316                        spam = True
317
318                # How to handle SPAM messages
319                #
320                if self.DROP_SPAM and spam:
321                        if self.DEBUG > 2 :
322                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
323
[204]324                        return 'drop'   
[194]325
326                elif spam:
327
[204]328                        return 'Spam'   
[67]329
[194]330                else:
[22]331
[204]332                        return False
[191]333
[221]334        def email_header_acl(self, keyword, header_field, default):
[206]335                """
[221]336                This function wil check if the email address is allowed or denied
337                to send mail to the ticket list
338            """
[206]339                try:
[221]340                        mail_addresses = self.parameters[keyword]
341
342                        # Check if we have an empty string
343                        #
344                        if not mail_addresses:
345                                return default
346
[206]347                except KeyError, detail:
[221]348                        if self.DEBUG > 2 :
[250]349                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
[206]350
[221]351                        return default
[206]352
[221]353                mail_addresses = string.split(mail_addresses, ',')
354
355                for entry in mail_addresses:
[209]356                        entry = entry.strip()
[221]357                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
358                        result =  TO_RE.search(header_field)
[208]359                        if result:
360                                return True
[149]361
[208]362                return False
363
[22]364        def email_header_txt(self, m):
[72]365                """
366                Display To and CC addresses in description field
367                """
[288]368                s = ''
[334]369
[213]370                if m['To'] and len(m['To']) > 0:
[288]371                        s = "'''To:''' %s\r\n" %(m['To'])
[22]372                if m['Cc'] and len(m['Cc']) > 0:
[288]373                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]374
[288]375                return  self.email_to_unicode(s)
[22]376
[138]377
[194]378        def get_sender_info(self, message):
[45]379                """
[72]380                Get the default author name and email address from the message
[226]381                """
[43]382
[226]383                self.email_to = self.email_to_unicode(message['to'])
384                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
385
[194]386                self.email_from = self.email_to_unicode(message['from'])
[287]387                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]388
[304]389                ## Trac can not handle author's name that contains spaces
390                #  and forbid the ticket email address as author field
[194]391
[305]392                if self.email_addr == self.trac_smtp_from:
[304]393                        self.author = "email2trac"
394                else:
395                        self.author = self.email_addr
396
[194]397                if self.IGNORE_TRAC_USER_SETTINGS:
398                        return
399
400                # Is this a registered user, use email address as search key:
401                # result:
402                #   u : login name
403                #   n : Name that the user has set in the settings tab
404                #   e : email address that the user has set in the settings tab
[45]405                #
[194]406                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]407                        if e and (e.lower() == self.email_addr.lower()) ]
[43]408
[45]409                if len(users) == 1:
[194]410                        self.email_from = users[0][0]
[250]411                        self.author = users[0][0]
[45]412
[72]413        def set_reply_fields(self, ticket, message):
414                """
415                Set all the right fields for a new ticket
416                """
[299]417                if self.DEBUG:
418                        print 'TD: set_reply_fields'
[72]419
[270]420                ## Only use name or email adress
421                #ticket['reporter'] = self.email_from
422                ticket['reporter'] = self.author
423
424
[45]425                # Put all CC-addresses in ticket CC field
[43]426                #
427                if self.REPLY_ALL:
428
[299]429                        email_cc = ''
430
431                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
432
433                        if not cc_addrs:
[105]434                                return
[43]435
[299]436                        ## Build a list of forbidden CC addresses
437                        #
[300]438                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
439                        #to_list = list()
440                        #for n,e in to_addrs:
441                        #       to_list.append(e)
[299]442                               
[43]443                        # Remove reporter email address if notification is
444                        # on
445                        #
446                        if self.notification:
447                                try:
[299]448                                        cc_addrs.remove((self.author, self.email_addr))
[43]449                                except ValueError, detail:
450                                        pass
451
[299]452                        for name,addr in cc_addrs:
453               
454                                ## Prevent mail loop
455                                #
[300]456                                #if addr in to_list:
[304]457
458                                if addr == self.trac_smtp_from:
[299]459                                        if self.DEBUG:
460                                                print "Skipping %s mail address for CC-field" %(addr)
461                                        continue
[43]462
[299]463                                if email_cc:
464                                        email_cc = '%s, %s' %(email_cc, addr)
465                                else:
466                                        email_cc = addr
[96]467
[299]468                        if email_cc:
469                                if self.DEBUG:
470                                        print 'TD: set_reply_fields: %s' %email_cc
471
472                                ticket['cc'] = self.email_to_unicode(email_cc)
473
[339]474
475########## DEBUG functions  ###########################################################
476
[310]477        def debug_body(self, message_body, tempfile=False):
478                if tempfile:
479                        import tempfile
480                        body_file = tempfile.mktemp('.email2trac')
481                else:
482                        body_file = os.path.join(self.TMPDIR, 'body.txt')
483
[331]484                if self.DRY_RUN:
485                        print 'DRY-RUN: not saving body to %s' %(body_file)
486                        return
487
488                print 'TD: writing body to %s' %(body_file)
489                fx = open(body_file, 'wb')
[310]490                if not message_body:
[331]491                                message_body = '(None)'
[310]492
493                message_body = message_body.encode('utf-8')
494                #message_body = unicode(message_body, 'iso-8859-15')
495
496                fx.write(message_body)
497                fx.close()
498                try:
499                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
500                except OSError:
501                        pass
502
503        def debug_attachments(self, message_parts):
[317]504                """
505                """
506                if self.VERBOSE:
507                        print "VB: debug_attachments"
508               
[310]509                n = 0
[331]510                for item in message_parts:
[310]511                        # Skip inline text parts
[331]512                        if not isinstance(item, tuple):
[310]513                                continue
514                               
[331]515                        (original, filename, part) = item
[310]516
517                        n = n + 1
518                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
[379]519               
520                        s = 'TD: part%d: filename: %s' %(n, filename)
521                        self.print_unicode(s)
522       
[348]523                        ## Forbidden chars
524                        #
525                        filename = filename.replace('\\', '_')
526                        filename = filename.replace('/', '_')
527       
528
529                        part_file = os.path.join(self.TMPDIR, filename)
[379]530                        s = 'TD: writing part%d (%s)' % (n,part_file)
531                        self.print_unicode(s)
[331]532
533                        if self.DRY_RUN:
534                                print 'DRY_RUN: NOT saving attachments'
535                                continue
536
[377]537                        part_file = util.text.unicode_quote(part_file)
538
[310]539                        fx = open(part_file, 'wb')
540                        text = part.get_payload(decode=1)
[331]541
[310]542                        if not text:
543                                text = '(None)'
[331]544
[310]545                        fx.write(text)
546                        fx.close()
[331]547
[310]548                        try:
549                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
550                        except OSError:
551                                pass
552
[96]553        def save_email_for_debug(self, message, tempfile=False):
[309]554
[96]555                if tempfile:
556                        import tempfile
557                        msg_file = tempfile.mktemp('.email2trac')
558                else:
[173]559                        #msg_file = '/var/tmp/msg.txt'
560                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
561
[331]562                if self.DRY_RUN:
563                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
564                else:
565                        print 'TD: saving email to %s' %(msg_file)
[44]566
[331]567                        fx = open(msg_file, 'wb')
568                        fx.write('%s' % message)
569                        fx.close()
570                       
571                        try:
572                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
573                        except OSError:
574                                pass
575
[309]576                message_parts = self.get_message_parts(message)
577                message_parts = self.unique_attachment_names(message_parts)
578                body_text = self.body_text(message_parts)
579                self.debug_body(body_text, True)
580                self.debug_attachments(message_parts)
581
[339]582########## Conversion functions  ###########################################################
583
[341]584        def email_to_unicode(self, message_str):
585                """
586                Email has 7 bit ASCII code, convert it to unicode with the charset
587                that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
588                understands it.
589                """
590                if self.VERBOSE:
591                        print "VB: email_to_unicode"
592
593                results =  email.Header.decode_header(message_str)
594
595                s = None
596                for text,format in results:
597                        if format:
598                                try:
599                                        temp = unicode(text, format)
600                                except UnicodeError, detail:
601                                        # This always works
602                                        #
603                                        temp = unicode(text, 'iso-8859-15')
604                                except LookupError, detail:
605                                        #text = 'ERROR: Could not find charset: %s, please install' %format
606                                        #temp = unicode(text, 'iso-8859-15')
607                                        temp = message_str
608                                       
609                        else:
610                                temp = string.strip(text)
611                                temp = unicode(text, 'iso-8859-15')
612
613                        if s:
614                                s = '%s %s' %(s, temp)
615                        else:
616                                s = '%s' %temp
617
618                #s = s.encode('utf-8')
619                return s
620
[288]621        def str_to_dict(self, s):
[164]622                """
[288]623                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]624                """
[359]625                if self.VERBOSE:
626                        print "VB: str_to_dict"
[164]627
[297]628                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
[262]629
[164]630                result = dict()
631                for field in fields:
632                        try:
[262]633                                index, value = string.split(field, '=')
[169]634
635                                # We can not change the description of a ticket via the subject
636                                # line. The description is the body of the email
637                                #
638                                if index.lower() in ['description']:
639                                        continue
640
[164]641                                if value:
[165]642                                        result[index.lower()] = value
[169]643
[164]644                        except ValueError:
645                                pass
[165]646                return result
[167]647
[379]648        def print_unicode(self,s):
649                """
650                This function prints unicode strings uif possible else it will quote it
651                """
652                try:
653                        print s
654                except UnicodeEncodeError, detail:
655                        print util.text.unicode_quote(s)
656
[339]657########## TRAC ticket functions  ###########################################################
658
[202]659        def update_ticket_fields(self, ticket, user_dict, use_default=None):
660                """
661                This will update the ticket fields. It will check if the
662                given fields are known and if the right values are specified
663                It will only update the ticket field value:
[169]664                        - If the field is known
[202]665                        - If the value supplied is valid for the ticket field.
666                          If not then there are two options:
667                           1) Skip the value (use_default=None)
668                           2) Set default value for field (use_default=1)
[169]669                """
[334]670                if self.VERBOSE:
671                        print "VB: update_ticket_fields"
[169]672
673                # Build a system dictionary from the ticket fields
674                # with field as index and option as value
675                #
676                sys_dict = dict()
677                for field in ticket.fields:
[167]678                        try:
[169]679                                sys_dict[field['name']] = field['options']
680
[167]681                        except KeyError:
[169]682                                sys_dict[field['name']] = None
[167]683                                pass
[169]684
[301]685                ## Check user supplied fields an compare them with the
[169]686                # system one's
687                #
688                for field,value in user_dict.items():
[202]689                        if self.DEBUG >= 10:
[379]690                                s = 'TD: user_field\t %s = %s' %(field,value)
691                                self.print_unicode(s)
[169]692
[301]693                        ## To prevent mail loop
694                        #
695                        if field == 'cc':
696
697                                cc_list = user_dict['cc'].split(',')
698
[304]699                                if self.trac_smtp_from in cc_list:
[301]700                                        if self.DEBUG > 10:
[304]701                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
702                                        cc_list.remove(self.trac_smtp_from)
[301]703
704                                value = ','.join(cc_list)
705                               
706
[169]707                        if sys_dict.has_key(field):
708
709                                # Check if value is an allowed system option, if TypeError then
710                                # every value is allowed
711                                #
712                                try:
713                                        if value in sys_dict[field]:
714                                                ticket[field] = value
[202]715                                        else:
716                                                # Must we set a default if value is not allowed
717                                                #
718                                                if use_default:
719                                                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]720
721                                except TypeError:
[345]722                                        pass
723
724                                ## Only set if we have a value
725                                #
726                                if value:
[169]727                                        ticket[field] = value
[202]728
729                                if self.DEBUG >= 10:
[379]730                                        s = 'ticket_field\t %s = %s' %(field,  ticket[field])
731                                        self.print_unicode(s)
[345]732
[260]733        def ticket_update(self, m, id, spam):
[78]734                """
[79]735                If the current email is a reply to an existing ticket, this function
736                will append the contents of this email to that ticket, instead of
737                creating a new one.
[78]738                """
[334]739                if self.VERBOSE:
740                        print "VB: ticket_update: %s" %id
[202]741
[164]742                # Must we update ticket fields
743                #
[220]744                update_fields = dict()
[165]745                try:
[260]746                        id, keywords = string.split(id, '?')
[262]747
[220]748                        update_fields = self.str_to_dict(keywords)
[165]749
750                        # Strip '#'
751                        #
[260]752                        self.id = int(id[1:])
[165]753
[260]754                except ValueError:
[390]755
[362]756                        # Strip '#'
[165]757                        #
[362]758                        self.id = int(id[1:])
[164]759
[362]760                if self.VERBOSE:
761                        print "VB: ticket_update: %s" %id
[71]762
[362]763
[194]764                # When is the change committed
765                #
[386]766                if self.VERSION < 0.11:
767                        when = int(time.time())
768                else:
[194]769                        utc = UTC()
770                        when = datetime.now(utc)
[77]771
[172]772                try:
[253]773                        tkt = Ticket(self.env, self.id, self.db)
[390]774
[172]775                except util.TracError, detail:
[390]776
[253]777                        # Not a valid ticket
[390]778
[253]779                        self.id = None
[172]780                        return False
[126]781
[288]782                # How many changes has this ticket
783                cnum = len(tkt.get_changelog())
784
785
[220]786                # reopen the ticket if it is was closed
787                # We must use the ticket workflow framework
788                #
[320]789                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
[220]790
[257]791                        #print controller.actions['reopen']
792                        #
793                        # As reference 
794                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
795                        #
796                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
797                        #print 'controller : ', a
798                        #
799                        #b = controller.get_all_status()
800                        #print 'get all status: ', b
801                        #
802                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
803                        #print 'get_ticket_changes :', b
804
[388]805                        if self.WORKFLOW and (self.VERSION >= 0.11 ) :
[257]806                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
807                                from trac.test import Mock, MockPerm
808
809                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
810
811                                controller = ConfigurableTicketWorkflow(self.env)
812                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
813
814                                if self.DEBUG:
815                                        print 'TD: Workflow ticket update fields: ', fields
816
817                                for key in fields.keys():
818                                        tkt[key] = fields[key]
819
820                        else:
821                                tkt['status'] = 'reopened'
822                                tkt['resolution'] = ''
823
[309]824                # Must we update some ticket fields properties via subjectline
[172]825                #
[220]826                if update_fields:
827                        self.update_ticket_fields(tkt, update_fields)
[166]828
[236]829                message_parts = self.get_message_parts(m)
[253]830                message_parts = self.unique_attachment_names(message_parts)
[210]831
[309]832                # Must we update some ticket fields properties via body_text
833                #
834                if self.properties:
835                                self.update_ticket_fields(tkt, self.properties)
836
[177]837                if self.EMAIL_HEADER:
[236]838                        message_parts.insert(0, self.email_header_txt(m))
[76]839
[236]840                body_text = self.body_text(message_parts)
841
[348]842                if self.VERSION  == 0.9:
843                        error_with_attachments = self.attach_attachments(message_parts, True)
844                else:
845                        error_with_attachments = self.attach_attachments(message_parts)
846
[309]847                if body_text.strip() or update_fields or self.properties:
[250]848                        if self.DRY_RUN:
[288]849                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]850                        else:
[348]851                                if error_with_attachments:
852                                        body_text = '%s\\%s' %(error_with_attachments, body_text)
853                               
[288]854                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
855                       
[219]856
[204]857                if self.notification and not spam:
[253]858                        self.notify(tkt, False, when)
[72]859
[71]860                return True
861
[202]862        def set_ticket_fields(self, ticket):
[77]863                """
[202]864                set the ticket fields to value specified
865                        - /etc/email2trac.conf with <prefix>_<field>
866                        - trac default values, trac.ini
867                """
868                user_dict = dict()
869
870                for field in ticket.fields:
871
872                        name = field['name']
873
[335]874                        ## default trac value
[202]875                        #
[233]876                        if not field.get('custom'):
877                                value = self.get_config('ticket', 'default_%s' %(name) )
878                        else:
[345]879                                ##  Else we get the default value for reporter
880                                #
[233]881                                value = field.get('value')
882                                options = field.get('options')
[345]883
[335]884                                if value and options and (value not in options):
[345]885                                         value = options[int(value)]
886       
[202]887                        if self.DEBUG > 10:
[379]888                                s = 'TD: trac.ini name %s = %s' %(name, value)
889                                self.print_unicode(s)
[202]890
[335]891                        ## email2trac.conf settings
892                        #
[206]893                        prefix = self.parameters['ticket_prefix']
[202]894                        try:
[206]895                                value = self.parameters['%s_%s' %(prefix, name)]
[202]896                                if self.DEBUG > 10:
[379]897                                        s = 'TD: email2trac.conf %s = %s ' %(name, value)
898                                        self.print_unicode(s)
[202]899
900                        except KeyError, detail:
901                                pass
902               
903                        if self.DEBUG:
[379]904                                s = 'TD: user_dict[%s] = %s' %(name, value)
905                                self.print_unicode(s)
[202]906
[345]907                        if value:
908                                user_dict[name] = value
[202]909
910                self.update_ticket_fields(ticket, user_dict, use_default=1)
911
[352]912                if 'status' not in user_dict.keys():
913                        ticket['status'] = 'new'
[202]914
915
[356]916        def ticket_update_by_subject(self, subject):
917                """
918                This list of Re: prefixes is probably incomplete. Taken from
919                wikipedia. Here is how the subject is matched
920                  - Re: <subject>
921                  - Re: (<Mail list label>:)+ <subject>
[202]922
[356]923                So we must have the last column
924                """
925                if self.VERBOSE:
926                        print "VB: ticket_update_by_subject()"
927
928                matched_id = None
929                if self.TICKET_UPDATE and self.TICKET_UPDATE_BY_SUBJECT:
930                               
931                        SUBJECT_RE = re.compile(r'^(RE|AW|VS|SV):(.*:)*\s*(.*)', re.IGNORECASE)
932                        result = SUBJECT_RE.search(subject)
933
934                        if result:
935                                # This is a reply
936                                orig_subject = result.group(3)
937
938                                if self.DEBUG:
939                                        print 'TD: subject search string: %s' %(orig_subject)
940
941                                cursor = self.db.cursor()
942                                summaries = [orig_subject, '%%: %s' % orig_subject]
943
[360]944                                ##
945                                # Convert days to seconds
946                                lookback = int(time.mktime(time.gmtime())) - \
947                                                self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK * 24 * 3600
[356]948
[360]949
[356]950                                for summary in summaries:
951                                        if self.DEBUG:
952                                                print 'TD: Looking for summary matching: "%s"' % summary
953                                        sql = """SELECT id FROM ticket
954                                                        WHERE changetime >= %s AND summary LIKE %s
955                                                        ORDER BY changetime DESC"""
956                                        cursor.execute(sql, [lookback, summary.strip()])
957
958                                        for row in cursor:
959                                                (matched_id,) = row
960                                                if self.DEBUG:
961                                                        print 'TD: Found matching ticket id: %d' % matched_id
962                                                break
963
964                                        if matched_id:
[366]965                                                matched_id = '#%d' % matched_id
[356]966                                                return matched_id
967
968                return matched_id
969
970
[262]971        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]972                """
[77]973                Create a new ticket
974                """
[356]975                if self.VERBOSE:
976                        print "VB: function new_ticket()"
[250]977
[41]978                tkt = Ticket(self.env)
[326]979
980                self.set_reply_fields(tkt, msg)
981
[202]982                self.set_ticket_fields(tkt)
983
984                # Old style setting for component, will be removed
985                #
[204]986                if spam:
987                        tkt['component'] = 'Spam'
988
[206]989                elif self.parameters.has_key('component'):
990                        tkt['component'] = self.parameters['component']
[201]991
[22]992                if not msg['Subject']:
[151]993                        tkt['summary'] = u'(No subject)'
[22]994                else:
[264]995                        tkt['summary'] = subject
[22]996
997
[262]998                if set_fields:
999                        rest, keywords = string.split(set_fields, '?')
1000
1001                        if keywords:
1002                                update_fields = self.str_to_dict(keywords)
1003                                self.update_ticket_fields(tkt, update_fields)
1004
[45]1005                # produce e-mail like header
1006                #
[22]1007                head = ''
1008                if self.EMAIL_HEADER > 0:
1009                        head = self.email_header_txt(msg)
[296]1010
[236]1011                message_parts = self.get_message_parts(msg)
[309]1012
1013                # Must we update some ticket fields properties via body_text
1014                #
1015                if self.properties:
1016                                self.update_ticket_fields(tkt, self.properties)
1017
[296]1018                if self.DEBUG:
1019                        print 'TD: self.get_message_parts ',
1020                        print message_parts
1021
[236]1022                message_parts = self.unique_attachment_names(message_parts)
[296]1023                if self.DEBUG:
1024                        print 'TD: self.unique_attachment_names',
1025                        print message_parts
[236]1026               
1027                if self.EMAIL_HEADER > 0:
1028                        message_parts.insert(0, self.email_header_txt(msg))
1029                       
1030                body_text = self.body_text(message_parts)
[45]1031
[236]1032                tkt['description'] = body_text
[90]1033
[182]1034                #when = int(time.time())
[192]1035                #
[182]1036                utc = UTC()
1037                when = datetime.now(utc)
[45]1038
[253]1039                if not self.DRY_RUN:
1040                        self.id = tkt.insert()
[273]1041       
[90]1042                changed = False
1043                comment = ''
[77]1044
[273]1045                # some routines in trac are dependend on ticket id     
1046                # like alternate notify template
1047                #
1048                if self.notify_template:
[274]1049                        tkt['id'] = self.id
[273]1050                        changed = True
1051
[295]1052                ## Rewrite the description if we have mailto enabled
[45]1053                #
[72]1054                if self.MAILTO:
[100]1055                        changed = True
[142]1056                        comment = u'\nadded mailto line\n'
[343]1057                        mailto = self.html_mailto_link( m['Subject'])
[253]1058
[213]1059                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]1060                                %(head, mailto, body_text)
[295]1061       
1062                ## Save the attachments to the ticket   
1063                #
[340]1064                error_with_attachments =  self.attach_attachments(message_parts)
[295]1065
[319]1066                if error_with_attachments:
1067                        changed = True
1068                        comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1069
[90]1070                if changed:
[204]1071                        if self.DRY_RUN:
[344]1072                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
[201]1073                        else:
[344]1074                                tkt.save_changes(tkt['reporter'], comment)
[201]1075                                #print tkt.get_changelog(self.db, when)
[90]1076
[250]1077                if self.notification and not spam:
[253]1078                        self.notify(tkt, True)
[45]1079
[260]1080
[342]1081        def attach_attachments(self, message_parts, update=False):
1082                '''
1083                save any attachments as files in the ticket's directory
1084                '''
1085                if self.VERBOSE:
1086                        print "VB: attach_attachments()"
1087
1088                if self.DRY_RUN:
1089                        print "DRY_RUN: no attachments attached to tickets"
1090                        return ''
1091
1092                count = 0
1093
1094                # Get Maxium attachment size
1095                #
1096                max_size = int(self.get_config('attachment', 'max_size'))
1097                status   = None
1098               
1099                for item in message_parts:
1100                        # Skip body parts
1101                        if not isinstance(item, tuple):
1102                                continue
1103                               
1104                        (original, filename, part) = item
1105                        #
[377]1106                        # We have to determine the size so we use this temporary solution. we must escape it
1107                        # else we get UnicodeErrors.
[342]1108                        #
[377]1109                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, util.text.unicode_quote(filename)))
[342]1110                        text = part.get_payload(decode=1)
1111                        if not text:
1112                                text = '(None)'
1113                        fd.write(text)
1114                        fd.close()
1115
1116                        # get the file_size
1117                        #
1118                        stats = os.lstat(path)
1119                        file_size = stats[stat.ST_SIZE]
1120
1121                        # Check if the attachment size is allowed
1122                        #
1123                        if (max_size != -1) and (file_size > max_size):
1124                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1125                                        %(status, original, file_size, max_size)
1126
1127                                os.unlink(path)
1128                                continue
1129                        else:
1130                                count = count + 1
1131                                       
1132                        # Insert the attachment
1133                        #
1134                        fd = open(path, 'rb')
[359]1135                        if self.system == 'discussion':
1136                                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1137                                  % (self.id,))
1138                        else:
1139                                att = attachment.Attachment(self.env, 'ticket', self.id)
1140 
[342]1141                        # This will break the ticket_update system, the body_text is vaporized
1142                        # ;-(
1143                        #
1144                        if not update:
1145                                att.author = self.author
1146                                att.description = self.email_to_unicode('Added by email2trac')
1147
[348]1148                        try:
1149                                att.insert(filename, fd, file_size)
1150                        except OSError, detail:
1151                                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
[342]1152
1153                        # Remove the created temporary filename
1154                        #
1155                        fd.close()
1156                        os.unlink(path)
1157
1158                ## return error
1159                #
1160                return status
1161
[359]1162########## Fullblog functions  #################################################
[339]1163
[260]1164        def blog(self, id):
1165                """
1166                The blog create/update function
1167                """
1168                # import the modules
1169                #
1170                from tracfullblog.core import FullBlogCore
[312]1171                from tracfullblog.model import BlogPost, BlogComment
1172                from trac.test import Mock, MockPerm
[260]1173
1174                # instantiate blog core
1175                blog = FullBlogCore(self.env)
[312]1176                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1177
[260]1178                if id:
1179
1180                        # update blog
1181                        #
[268]1182                        comment = BlogComment(self.env, id)
[260]1183                        comment.author = self.author
[312]1184
1185                        message_parts = self.get_message_parts(m)
1186                        comment.comment = self.body_text(message_parts)
1187
[260]1188                        blog.create_comment(req, comment)
1189
1190                else:
1191                        # create blog
1192                        #
1193                        import time
1194                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1195
1196                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1197                       
1198                        post.author = self.author
1199                        post.title = self.email_to_unicode(m['Subject'])
[312]1200
1201                        message_parts = self.get_message_parts(m)
1202                        post.body = self.body_text(message_parts)
[260]1203                       
1204                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1205
1206
[359]1207########## Discussion functions  ##############################################
[342]1208
[359]1209        def discussion_topic(self, content, subject):
[342]1210
[359]1211                # Import modules.
1212                from tracdiscussion.api import DiscussionApi
1213                from trac.util.datefmt import to_timestamp, utc
1214
1215                if self.DEBUG:
1216                        print 'TD: Creating a new topic in forum:', self.id
1217
1218                # Get dissussion API component.
1219                api = self.env[DiscussionApi]
1220                context = self._create_context(content, subject)
1221
1222                # Get forum for new topic.
1223                forum = api.get_forum(context, self.id)
1224
1225                if not forum and self.DEBUG:
1226                        print 'ERROR: Replied forum doesn\'t exist'
1227
1228                # Prepare topic.
1229                topic = {'forum' : forum['id'],
1230                                 'subject' : context.subject,
1231                                 'time': to_timestamp(datetime.now(utc)),
1232                                 'author' : self.author,
1233                                 'subscribers' : [self.email_addr],
1234                                 'body' : self.body_text(context.content_parts)}
1235
1236                # Add topic to DB and commit it.
1237                self._add_topic(api, context, topic)
1238                self.db.commit()
1239
1240        def discussion_topic_reply(self, content, subject):
1241
1242                # Import modules.
1243                from tracdiscussion.api import DiscussionApi
1244                from trac.util.datefmt import to_timestamp, utc
1245
1246                if self.DEBUG:
1247                        print 'TD: Replying to discussion topic', self.id
1248
1249                # Get dissussion API component.
1250                api = self.env[DiscussionApi]
1251                context = self._create_context(content, subject)
1252
1253                # Get replied topic.
1254                topic = api.get_topic(context, self.id)
1255
1256                if not topic and self.DEBUG:
1257                        print 'ERROR: Replied topic doesn\'t exist'
1258
1259                # Prepare message.
1260                message = {'forum' : topic['forum'],
1261                                   'topic' : topic['id'],
1262                                   'replyto' : -1,
1263                                   'time' : to_timestamp(datetime.now(utc)),
1264                                   'author' : self.author,
1265                                   'body' : self.body_text(context.content_parts)}
1266
1267                # Add message to DB and commit it.
1268                self._add_message(api, context, message)
1269                self.db.commit()
1270
1271        def discussion_message_reply(self, content, subject):
1272
1273                # Import modules.
1274                from tracdiscussion.api import DiscussionApi
1275                from trac.util.datefmt import to_timestamp, utc
1276
1277                if self.DEBUG:
1278                        print 'TD: Replying to discussion message', self.id
1279
1280                # Get dissussion API component.
1281                api = self.env[DiscussionApi]
1282                context = self._create_context(content, subject)
1283
1284                # Get replied message.
1285                message = api.get_message(context, self.id)
1286
1287                if not message and self.DEBUG:
1288                        print 'ERROR: Replied message doesn\'t exist'
1289
1290                # Prepare message.
1291                message = {'forum' : message['forum'],
1292                                   'topic' : message['topic'],
1293                                   'replyto' : message['id'],
1294                                   'time' : to_timestamp(datetime.now(utc)),
1295                                   'author' : self.author,
1296                                   'body' : self.body_text(context.content_parts)}
1297
1298                # Add message to DB and commit it.
1299                self._add_message(api, context, message)
1300                self.db.commit()
1301
1302        def _create_context(self, content, subject):
1303
1304                # Import modules.
1305                from trac.mimeview import Context
1306                from trac.web.api import Request
1307                from trac.perm import PermissionCache
1308
1309                # TODO: Read server base URL from config.
1310                # Create request object to mockup context creation.
1311                #
1312                environ = {'SERVER_PORT' : 80,
1313                                   'SERVER_NAME' : 'test',
1314                                   'REQUEST_METHOD' : 'POST',
1315                                   'wsgi.url_scheme' : 'http',
1316                                   'wsgi.input' : sys.stdin}
1317                chrome =  {'links': {},
1318                                   'scripts': [],
1319                                   'ctxtnav': [],
1320                                   'warnings': [],
1321                                   'notices': []}
1322
1323                if self.env.base_url_for_redirect:
1324                        environ['trac.base_url'] = self.env.base_url
1325
1326                req = Request(environ, None)
1327                req.chrome = chrome
1328                req.tz = 'missing'
1329                req.authname = self.author
1330                req.perm = PermissionCache(self.env, self.author)
1331
1332                # Create and return context.
1333                context = Context.from_request(req)
1334                context.realm = 'discussion-email2trac'
1335                context.cursor = self.db.cursor()
1336                context.content = content
1337                context.subject = subject
1338
1339                # Read content parts from content.
1340                context.content_parts = self.get_message_parts(content)
1341                context.content_parts = self.unique_attachment_names(
1342                  context.content_parts)
1343
1344                return context
1345
1346        def _add_topic(self, api, context, topic):
1347                context.req.perm.assert_permission('DISCUSSION_APPEND')
1348
1349                # Filter topic.
1350                for discussion_filter in api.discussion_filters:
1351                        accept, topic_or_error = discussion_filter.filter_topic(
1352                          context, topic)
1353                        if accept:
1354                                topic = topic_or_error
1355                        else:
1356                                raise TracError(topic_or_error)
1357
1358                # Add a new topic.
1359                api.add_topic(context, topic)
1360
1361                # Get inserted topic with new ID.
1362                topic = api.get_topic_by_time(context, topic['time'])
1363
1364                # Attach attachments.
1365                self.id = topic['id']
1366                self.attach_attachments(context.content_parts, self.VERSION == 0.9)
1367
1368                # Notify change listeners.
1369                for listener in api.topic_change_listeners:
1370                        listener.topic_created(context, topic)
1371
1372        def _add_message(self, api, context, message):
1373                context.req.perm.assert_permission('DISCUSSION_APPEND')
1374
1375                # Filter message.
1376                for discussion_filter in api.discussion_filters:
1377                        accept, message_or_error = discussion_filter.filter_message(
1378                          context, message)
1379                        if accept:
1380                                message = message_or_error
1381                        else:
1382                                raise TracError(message_or_error)
1383
1384                # Add message.
1385                api.add_message(context, message)
1386
1387                # Get inserted message with new ID.
1388                message = api.get_message_by_time(context, message['time'])
1389
1390                # Attach attachments.
1391                self.id = message['topic']
1392                self.attach_attachments(context.content_parts, self.VERSION == 0.9)
1393
1394                # Notify change listeners.
1395                for listener in api.message_change_listeners:
1396                        listener.message_created(context, message)
1397
1398########## MAIN function  ######################################################
1399
[77]1400        def parse(self, fp):
[356]1401                """
1402                """
1403                if self.VERBOSE:
1404                        print "VB: main function parse()"
[96]1405                global m
1406
[77]1407                m = email.message_from_file(fp)
[239]1408               
[77]1409                if not m:
[221]1410                        if self.DEBUG:
[250]1411                                print "TD: This is not a valid email message format"
[77]1412                        return
[239]1413                       
1414                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
[316]1415                try:
1416                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1417                except AttributeError, detail:
1418                        pass
[239]1419
[77]1420                if self.DEBUG > 1:        # save the entire e-mail message text
[219]1421                        self.save_email_for_debug(m, True)
[77]1422
1423                self.db = self.env.get_db_cnx()
[194]1424                self.get_sender_info(m)
[152]1425
[221]1426                if not self.email_header_acl('white_list', self.email_addr, True):
1427                        if self.DEBUG > 1 :
1428                                print 'Message rejected : %s not in white list' %(self.email_addr)
1429                        return False
[77]1430
[221]1431                if self.email_header_acl('black_list', self.email_addr, False):
1432                        if self.DEBUG > 1 :
1433                                print 'Message rejected : %s in black list' %(self.email_addr)
1434                        return False
1435
[227]1436                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]1437                        if self.DEBUG > 1 :
1438                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1439                        return False
1440
[204]1441                # If drop the message
[194]1442                #
[204]1443                if self.spam(m) == 'drop':
[194]1444                        return False
1445
[204]1446                elif self.spam(m) == 'spam':
1447                        spam_msg = True
1448                else:
1449                        spam_msg = False
1450
[77]1451                if self.get_config('notification', 'smtp_enabled') in ['true']:
1452                        self.notification = 1
1453                else:
1454                        self.notification = 0
1455
[359]1456                if not m['Subject']:
1457                        subject  = 'No Subject'
1458                else:
1459                        subject  = self.email_to_unicode(m['Subject'])
[304]1460
[359]1461                if self.DEBUG:
1462                         print "TD:", subject
1463
1464                #
1465                # [hic] #1529: Re: LRZ
1466                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1467                #
1468                ticket_regex = r'''
1469                        (?P<new_fields>[#][?].*)
[390]1470                        |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
[359]1471                        '''
[260]1472                # Check if  FullBlogPlugin is installed
[77]1473                #
[260]1474                blog_enabled = None
[359]1475                blog_regex = ''
[260]1476                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1477                        blog_enabled = True
[359]1478                        blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1479
[77]1480
[359]1481                # Check if DiscussionPlugin is installed
[260]1482                #
[359]1483                discussion_enabled = None
1484                discussion_regex = ''
1485                if self.get_config('components', 'tracdiscussion.api.*') in ['enabled']:
1486                        discussion_enabled = True
1487                        discussion_regex = r'''
1488                        |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1489                        |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1490                        |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1491                        '''
[77]1492
[359]1493
1494                regex_str = ticket_regex + blog_regex + discussion_regex
1495                SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1496
1497                # Find out if this is a ticket, a blog or a discussion
[265]1498                #
[359]1499                result =  SYSTEM_RE.search(subject)
[390]1500
[260]1501                if result:
1502                        # update ticket + fields
1503                        #
[359]1504                        if result.group('reply') and self.TICKET_UPDATE:
1505                                self.system = 'ticket'
[260]1506
[390]1507                                # Skip the last ':' character
1508                                #
1509                                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1510                                        self.new_ticket(m, subject, spam_msg)
1511
[262]1512                        # New ticket + fields
1513                        #
1514                        elif result.group('new_fields'):
[359]1515                                self.system = 'ticket'
[262]1516                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1517
[359]1518                        if blog_enabled:
1519                                if result.group('blog'):
1520                                        self.system = 'blog'
1521                                        self.blog(result.group('blog_id'))
1522
1523                        if discussion_enabled:
1524                                # New topic.
1525                                #
1526                                if result.group('forum'):
1527                                        self.system = 'discussion'
1528                                        self.id = int(result.group('forum_id'))
1529                                        self.discussion_topic(m, subject[result.end('forum'):])
1530
1531                                # Reply to topic.
1532                                #
1533                                elif result.group('topic'):
1534                                        self.system = 'discussion'
1535                                        self.id = int(result.group('topic_id'))
1536                                        self.discussion_topic_reply(m, subject[result.end('topic'):])
1537
1538                                # Reply to topic message.
1539                                #
1540                                elif result.group('message'):
1541                                        self.system = 'discussion'
1542                                        self.id = int(result.group('message_id'))
1543                                        self.discussion_message_reply(m, subject[result.end('message'):])
1544
[260]1545                else:
[359]1546                        self.system = 'ticket'
[356]1547                        result = self.ticket_update_by_subject(subject)
1548                        if result:
[390]1549                                if not self.ticket_update(m, result, spam_msg):
1550                                        self.new_ticket(m, subject, spam_msg)
[353]1551                        else:
1552                                # No update by subject, so just create a new ticket
1553                                self.new_ticket(m, subject, spam_msg)
1554
[356]1555
[343]1556########## BODY TEXT functions  ###########################################################
1557
[136]1558        def strip_signature(self, text):
1559                """
1560                Strip signature from message, inspired by Mailman software
1561                """
1562                body = []
1563                for line in text.splitlines():
1564                        if line == '-- ':
1565                                break
1566                        body.append(line)
1567
1568                return ('\n'.join(body))
1569
[231]1570        def reflow(self, text, delsp = 0):
1571                """
1572                Reflow the message based on the format="flowed" specification (RFC 3676)
1573                """
1574                flowedlines = []
1575                quotelevel = 0
1576                prevflowed = 0
1577
1578                for line in text.splitlines():
1579                        from re import match
1580                       
1581                        # Figure out the quote level and the content of the current line
1582                        m = match('(>*)( ?)(.*)', line)
1583                        linequotelevel = len(m.group(1))
1584                        line = m.group(3)
1585
1586                        # Determine whether this line is flowed
1587                        if line and line != '-- ' and line[-1] == ' ':
1588                                flowed = 1
1589                        else:
1590                                flowed = 0
1591
1592                        if flowed and delsp and line and line[-1] == ' ':
1593                                line = line[:-1]
1594
1595                        # If the previous line is flowed, append this line to it
1596                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1597                                flowedlines[-1] += line
1598                        # Otherwise, start a new line
1599                        else:
1600                                flowedlines.append('>' * linequotelevel + line)
1601
1602                        prevflowed = flowed
1603                       
1604
1605                return '\n'.join(flowedlines)
1606
[191]1607        def strip_quotes(self, text):
[193]1608                """
1609                Strip quotes from message by Nicolas Mendoza
1610                """
1611                body = []
1612                for line in text.splitlines():
1613                        if line.startswith(self.EMAIL_QUOTE):
1614                                continue
1615                        body.append(line)
[151]1616
[193]1617                return ('\n'.join(body))
[191]1618
[309]1619        def inline_properties(self, text):
1620                """
1621                Parse text if we use inline keywords to set ticket fields
1622                """
1623                if self.DEBUG:
1624                        print 'TD: inline_properties function'
1625
1626                properties = dict()
1627                body = list()
1628
1629                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1630
1631                for line in text.splitlines():
1632                        match = INLINE_EXP.match(line)
1633                        if match:
1634                                keyword, value = match.groups()
1635                                self.properties[keyword] = value.strip()
[311]1636                                if self.DEBUG:
1637                                        print "TD: inline properties: %s : %s" %(keyword,value)
[309]1638                        else:
1639                                body.append(line)
1640                               
1641                return '\n'.join(body)
1642
1643
[154]1644        def wrap_text(self, text, replace_whitespace = False):
[151]1645                """
[191]1646                Will break a lines longer then given length into several small
1647                lines of size given length
[151]1648                """
1649                import textwrap
[154]1650
[151]1651                LINESEPARATOR = '\n'
[153]1652                reformat = ''
[151]1653
[154]1654                for s in text.split(LINESEPARATOR):
1655                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1656                        if tmp:
1657                                reformat = '%s\n%s' %(reformat,tmp)
1658                        else:
1659                                reformat = '%s\n' %reformat
[153]1660
1661                return reformat
1662
[154]1663                # Python2.4 and higher
1664                #
1665                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1666                #
1667
[343]1668########## EMAIL attachements functions ###########################################################
1669
[340]1670        def inline_part(self, part):
1671                """
1672                """
1673                if self.VERBOSE:
1674                        print "VB: inline_part()"
[154]1675
[340]1676                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1677
[236]1678        def get_message_parts(self, msg):
[45]1679                """
[236]1680                parses the email message and returns a list of body parts and attachments
1681                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1682                """
[317]1683                if self.VERBOSE:
1684                        print "VB: get_message_parts()"
1685
[309]1686                message_parts = list()
[294]1687       
[278]1688                ALTERNATIVE_MULTIPART = False
1689
[22]1690                for part in msg.walk():
[236]1691                        if self.DEBUG:
[278]1692                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
[236]1693                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[278]1694
1695                        ## Check content type
[294]1696                        #
1697                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[238]1698
[294]1699                                if self.DEBUG:
1700                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
[238]1701
1702                                continue
1703
[294]1704                        ## Catch some mulitpart execptions
1705                        #
1706                        if part.get_content_type() == 'multipart/alternative':
[278]1707                                ALTERNATIVE_MULTIPART = True
1708                                continue
1709
[294]1710                        ## Skip multipart containers
[278]1711                        #
[45]1712                        if part.get_content_maintype() == 'multipart':
[278]1713                                if self.DEBUG:
1714                                        print "TD: Skipping multipart container"
[22]1715                                continue
[278]1716                       
[294]1717                        ## 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"
1718                        #
[236]1719                        inline = self.inline_part(part)
1720
[294]1721                        ## Drop HTML message
1722                        #
[278]1723                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1724                                if part.get_content_type() == 'text/html':
1725                                        if self.DEBUG:
1726                                                print "TD: Skipping alternative HTML message"
1727
1728                                        ALTERNATIVE_MULTIPART = False
1729                                        continue
1730
[294]1731                        ## Inline text parts are where the body is
1732                        #
[236]1733                        if part.get_content_type() == 'text/plain' and inline:
1734                                if self.DEBUG:
1735                                        print 'TD:               Inline body part'
1736
[45]1737                                # Try to decode, if fails then do not decode
1738                                #
[90]1739                                body_text = part.get_payload(decode=1)
[45]1740                                if not body_text:                       
[90]1741                                        body_text = part.get_payload(decode=0)
[231]1742
[232]1743                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1744                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1745
1746                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1747                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1748       
[136]1749                                if self.STRIP_SIGNATURE:
1750                                        body_text = self.strip_signature(body_text)
[22]1751
[191]1752                                if self.STRIP_QUOTES:
1753                                        body_text = self.strip_quotes(body_text)
1754
[309]1755                                if self.INLINE_PROPERTIES:
1756                                        body_text = self.inline_properties(body_text)
1757
[148]1758                                if self.USE_TEXTWRAP:
[151]1759                                        body_text = self.wrap_text(body_text)
[148]1760
[294]1761                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1762                                #
[100]1763                                charset = part.get_content_charset()
[102]1764                                if not charset:
1765                                        charset = 'iso-8859-15'
1766
[89]1767                                try:
[96]1768                                        ubody_text = unicode(body_text, charset)
[100]1769
1770                                except UnicodeError, detail:
[96]1771                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1772
[100]1773                                except LookupError, detail:
[139]1774                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1775
[236]1776                                if self.VERBATIM_FORMAT:
1777                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1778                                else:
1779                                        message_parts.append('%s' %ubody_text)
1780                        else:
1781                                if self.DEBUG:
[379]1782                                        s = 'TD:               Filename: %s' % part.get_filename()
1783                                        self.print_unicode(s)
[22]1784
[383]1785                                ##
1786                                #  First try to use email header function to convert filename.
1787                                #  If this fails the use the plan filename
1788                                try:
1789                                        filename = self.email_to_unicode(part.get_filename())
1790                                except UnicodeEncodeError, detail:
1791                                        filename = part.get_filename()
1792
[317]1793                                message_parts.append((filename, part))
[236]1794
1795                return message_parts
1796               
[253]1797        def unique_attachment_names(self, message_parts):
[296]1798                """
1799                """
[236]1800                renamed_parts = []
1801                attachment_names = set()
[296]1802
[331]1803                for item in message_parts:
[236]1804                       
[296]1805                        ## If not an attachment, leave it alone
1806                        #
[331]1807                        if not isinstance(item, tuple):
1808                                renamed_parts.append(item)
[236]1809                                continue
1810                               
[331]1811                        (filename, part) = item
[295]1812
[296]1813                        ## If no filename, use a default one
1814                        #
1815                        if not filename:
[236]1816                                filename = 'untitled-part'
[22]1817
[242]1818                                # Guess the extension from the content type, use non strict mode
1819                                # some additional non-standard but commonly used MIME types
1820                                # are also recognized
1821                                #
1822                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1823                                if not ext:
1824                                        ext = '.bin'
[22]1825
[236]1826                                filename = '%s%s' % (filename, ext)
[22]1827
[348]1828                        ## Discard relative paths for windows/unix in attachment names
[296]1829                        #
[348]1830                        #filename = filename.replace('\\', '/').replace(':', '/')
1831                        filename = filename.replace('\\', '_')
1832                        filename = filename.replace('/', '_')
[347]1833
[296]1834                        #
[236]1835                        # We try to normalize the filename to utf-8 NFC if we can.
1836                        # Files uploaded from OS X might be in NFD.
1837                        # Check python version and then try it
1838                        #
[348]1839                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1840                        #       try:
1841                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1842                        #       except TypeError:
1843                        #               pass
[100]1844
[236]1845                        # Make the filename unique for this ticket
1846                        num = 0
1847                        unique_filename = filename
[296]1848                        dummy_filename, ext = os.path.splitext(filename)
[134]1849
[339]1850                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
[236]1851                                num += 1
[296]1852                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]1853                               
1854                        if self.DEBUG:
[379]1855                                s = 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1856                                self.print_unicode(s)
[100]1857
[236]1858                        attachment_names.add(unique_filename)
1859
1860                        renamed_parts.append((filename, unique_filename, part))
[296]1861       
[236]1862                return renamed_parts
1863                       
1864                       
[253]1865        def attachment_exists(self, filename):
[250]1866
1867                if self.DEBUG:
[379]1868                        s = 'TD: attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
1869                        self.print_unicode(s)
[250]1870
1871                # We have no valid ticket id
1872                #
[253]1873                if not self.id:
[236]1874                        return False
[250]1875
[236]1876                try:
[359]1877                        if self.system == 'discussion':
1878                                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
1879                                  % (self.id,), filename)
1880                        else:
1881                                att = attachment.Attachment(self.env, 'ticket', self.id,
1882                                  filename)
[236]1883                        return True
[250]1884                except attachment.ResourceNotFound:
[236]1885                        return False
[343]1886
1887########## TRAC Ticket Text ###########################################################
[236]1888                       
1889        def body_text(self, message_parts):
1890                body_text = []
1891               
1892                for part in message_parts:
1893                        # Plain text part, append it
1894                        if not isinstance(part, tuple):
1895                                body_text.extend(part.strip().splitlines())
1896                                body_text.append("")
1897                                continue
1898                               
1899                        (original, filename, part) = part
1900                        inline = self.inline_part(part)
1901                       
1902                        if part.get_content_maintype() == 'image' and inline:
[359]1903                                if self.system != 'discussion':
1904                                        body_text.append('[[Image(%s)]]' % filename)
[236]1905                                body_text.append("")
1906                        else:
[359]1907                                if self.system != 'discussion':
1908                                        body_text.append('[attachment:"%s"]' % filename)
[236]1909                                body_text.append("")
1910                               
1911                body_text = '\r\n'.join(body_text)
1912                return body_text
1913
[343]1914        def html_mailto_link(self, subject):
1915                """
1916                This function returns a HTML mailto tag with the ticket id and author email address
1917                """
1918                if not self.author:
1919                        author = self.email_addr
1920                else:   
1921                        author = self.author
1922
1923                # use urllib to escape the chars
1924                #
1925                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1926                       urllib.quote(self.email_addr),
1927                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1928                           urllib.quote(self.MAILTO_CC)
1929                           )
1930
1931                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
1932                return s
1933
1934########## TRAC notify section ###########################################################
1935
[253]1936        def notify(self, tkt, new=True, modtime=0):
[79]1937                """
1938                A wrapper for the TRAC notify function. So we can use templates
1939                """
[250]1940                if self.DRY_RUN:
[344]1941                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
[250]1942                                return
[41]1943                try:
1944                        # create false {abs_}href properties, to trick Notify()
1945                        #
[369]1946                        if not (self.VERSION in [0.11, 0.12]):
[192]1947                                self.env.abs_href = Href(self.get_config('project', 'url'))
1948                                self.env.href = Href(self.get_config('project', 'url'))
[22]1949
[41]1950                        tn = TicketNotifyEmail(self.env)
[213]1951
[42]1952                        if self.notify_template:
[222]1953
[388]1954                                if self.VERSION >= 0.11:
[222]1955
[221]1956                                        from trac.web.chrome import Chrome
[222]1957
1958                                        if self.notify_template_update and not new:
1959                                                tn.template_name = self.notify_template_update
1960                                        else:
1961                                                tn.template_name = self.notify_template
1962
[221]1963                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1964                                               
1965                                else:
[222]1966
[221]1967                                        tn.template_name = self.notify_template;
[42]1968
[77]1969                        tn.notify(tkt, new, modtime)
[41]1970
1971                except Exception, e:
[253]1972                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
[41]1973
[22]1974
[74]1975
[343]1976########## Parse Config File  ###########################################################
[22]1977
1978def ReadConfig(file, name):
1979        """
1980        Parse the config file
1981        """
1982        if not os.path.isfile(file):
[79]1983                print 'File %s does not exist' %file
[22]1984                sys.exit(1)
1985
[199]1986        config = trac_config.Configuration(file)
[22]1987
1988        # Use given project name else use defaults
1989        #
1990        if name:
[199]1991                sections = config.sections()
1992                if not name in sections:
[79]1993                        print "Not a valid project name: %s" %name
[199]1994                        print "Valid names: %s" %sections
[22]1995                        sys.exit(1)
1996
1997                project =  dict()
[199]1998                for option, value in  config.options(name):
1999                        project[option] = value
[22]2000
2001        else:
[270]2002                # use some trac internals to get the defaults
[217]2003                #
2004                project = config.parser.defaults()
[22]2005
2006        return project
2007
[87]2008
[22]2009if __name__ == '__main__':
2010        # Default config file
2011        #
[24]2012        configfile = '@email2trac_conf@'
[22]2013        project = ''
2014        component = ''
[202]2015        ticket_prefix = 'default'
[204]2016        dry_run = None
[317]2017        verbose = None
[202]2018
[87]2019        ENABLE_SYSLOG = 0
[201]2020
[317]2021        SHORT_OPT = 'chf:np:t:v'
2022        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2023
[22]2024        try:
[201]2025                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]2026        except getopt.error,detail:
2027                print __doc__
2028                print detail
2029                sys.exit(1)
[87]2030       
[22]2031        project_name = None
2032        for opt,value in opts:
2033                if opt in [ '-h', '--help']:
2034                        print __doc__
2035                        sys.exit(0)
2036                elif opt in ['-c', '--component']:
2037                        component = value
2038                elif opt in ['-f', '--file']:
2039                        configfile = value
[201]2040                elif opt in ['-n', '--dry-run']:
[204]2041                        dry_run = True
[22]2042                elif opt in ['-p', '--project']:
2043                        project_name = value
[202]2044                elif opt in ['-t', '--ticket_prefix']:
2045                        ticket_prefix = value
[388]2046                elif opt in ['-v', '--verbose']:
[317]2047                        verbose = True
[87]2048       
[22]2049        settings = ReadConfig(configfile, project_name)
2050        if not settings.has_key('project'):
2051                print __doc__
[79]2052                print 'No Trac project is defined in the email2trac config file.'
[22]2053                sys.exit(1)
[87]2054       
[22]2055        if component:
2056                settings['component'] = component
[202]2057
2058        # The default prefix for ticket values in email2trac.conf
2059        #
2060        settings['ticket_prefix'] = ticket_prefix
[206]2061        settings['dry_run'] = dry_run
[317]2062        settings['verbose'] = verbose
[22]2063
[189]2064
[363]2065        # Determine major trac version used to be in email2trac.conf
[373]2066        # Quick hack for 0.12
[363]2067        #
2068        version = '0.%s' %(trac_version.split('.')[1])
[373]2069        if version.startswith('0.12'):
2070                version = '0.12'
2071
[363]2072        if verbose:
2073                print "Found trac version: %s" %(version)
2074       
[22]2075        #debug HvB
2076        #print settings
[189]2077
[87]2078        try:
[189]2079                if version == '0.9':
[87]2080                        from trac import attachment
2081                        from trac.env import Environment
2082                        from trac.ticket import Ticket
2083                        from trac.web.href import Href
2084                        from trac import util
2085                        from trac.Notify import TicketNotifyEmail
[189]2086                elif version == '0.10':
[87]2087                        from trac import attachment
2088                        from trac.env import Environment
2089                        from trac.ticket import Ticket
2090                        from trac.web.href import Href
2091                        from trac import util
[139]2092                        #
2093                        # return  util.text.to_unicode(str)
2094                        #
[87]2095                        # see http://projects.edgewall.com/trac/changeset/2799
2096                        from trac.ticket.notification import TicketNotifyEmail
[199]2097                        from trac import config as trac_config
[359]2098                        from trac.core import TracError
2099
[189]2100                elif version == '0.11':
[182]2101                        from trac import attachment
2102                        from trac.env import Environment
2103                        from trac.ticket import Ticket
2104                        from trac.web.href import Href
[199]2105                        from trac import config as trac_config
[182]2106                        from trac import util
[359]2107                        from trac.core import TracError
[260]2108
[368]2109                        #
2110                        # return  util.text.to_unicode(str)
2111                        #
2112                        # see http://projects.edgewall.com/trac/changeset/2799
2113                        from trac.ticket.notification import TicketNotifyEmail
[260]2114
[368]2115                elif version == '0.12':
2116                        from trac import attachment
2117                        from trac.env import Environment
2118                        from trac.ticket import Ticket
2119                        from trac.web.href import Href
2120                        from trac import config as trac_config
2121                        from trac import util
2122                        from trac.core import TracError
2123
[182]2124                        #
2125                        # return  util.text.to_unicode(str)
2126                        #
2127                        # see http://projects.edgewall.com/trac/changeset/2799
2128                        from trac.ticket.notification import TicketNotifyEmail
[368]2129
2130
[189]2131                else:
2132                        print 'TRAC version %s is not supported' %version
2133                        sys.exit(1)
2134                       
2135                if settings.has_key('enable_syslog'):
[190]2136                        if SYSLOG_AVAILABLE:
2137                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]2138
[291]2139
2140                # Must be set before environment is created
2141                #
2142                if settings.has_key('python_egg_cache'):
2143                        python_egg_cache = str(settings['python_egg_cache'])
2144                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2145
[359]2146       
2147                if int(settings['debug']) > 0:
2148                        print 'Loading environment', settings['project']
2149
[87]2150                env = Environment(settings['project'], create=0)
[333]2151
[206]2152                tktparser = TicketEmailParser(env, settings, float(version))
[87]2153                tktparser.parse(sys.stdin)
[22]2154
[87]2155        # Catch all errors ans log to SYSLOG if we have enabled this
2156        # else stdout
2157        #
2158        except Exception, error:
2159                if ENABLE_SYSLOG:
2160                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]2161
[87]2162                        etype, evalue, etb = sys.exc_info()
2163                        for e in traceback.format_exception(etype, evalue, etb):
2164                                syslog.syslog(e)
[187]2165
[87]2166                        syslog.closelog()
2167                else:
2168                        traceback.print_exc()
[22]2169
[97]2170                if m:
[98]2171                        tktparser.save_email_for_debug(m, True)
[97]2172
[356]2173
[249]2174                sys.exit(1)
[22]2175# EOB
Note: See TracBrowser for help on using the repository browser.