source: trunk/email2trac.py.in @ 389

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

Enabled workflow and notification for 0.12

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 54.5 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 388 2010-06-28 06:23:18Z 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
748                        # Skip the last ':' character
749                        #
750                        keywords = keywords[:-1]
[220]751                        update_fields = self.str_to_dict(keywords)
[165]752
753                        # Strip '#'
754                        #
[260]755                        self.id = int(id[1:])
[165]756
[260]757                except ValueError:
[362]758                        # Strip '#'
[165]759                        #
[362]760                        self.id = int(id[1:])
[164]761
[362]762                if self.VERBOSE:
763                        print "VB: ticket_update: %s" %id
[71]764
[362]765
[194]766                # When is the change committed
767                #
[386]768                if self.VERSION < 0.11:
769                        when = int(time.time())
770                else:
[194]771                        utc = UTC()
772                        when = datetime.now(utc)
[77]773
[172]774                try:
[253]775                        tkt = Ticket(self.env, self.id, self.db)
[172]776                except util.TracError, detail:
[253]777                        # Not a valid ticket
778                        self.id = None
[172]779                        return False
[126]780
[288]781                # How many changes has this ticket
782                cnum = len(tkt.get_changelog())
783
784
[220]785                # reopen the ticket if it is was closed
786                # We must use the ticket workflow framework
787                #
[320]788                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
[220]789
[257]790                        #print controller.actions['reopen']
791                        #
792                        # As reference 
793                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
794                        #
795                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
796                        #print 'controller : ', a
797                        #
798                        #b = controller.get_all_status()
799                        #print 'get all status: ', b
800                        #
801                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
802                        #print 'get_ticket_changes :', b
803
[388]804                        if self.WORKFLOW and (self.VERSION >= 0.11 ) :
[257]805                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
806                                from trac.test import Mock, MockPerm
807
808                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
809
810                                controller = ConfigurableTicketWorkflow(self.env)
811                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
812
813                                if self.DEBUG:
814                                        print 'TD: Workflow ticket update fields: ', fields
815
816                                for key in fields.keys():
817                                        tkt[key] = fields[key]
818
819                        else:
820                                tkt['status'] = 'reopened'
821                                tkt['resolution'] = ''
822
[309]823                # Must we update some ticket fields properties via subjectline
[172]824                #
[220]825                if update_fields:
826                        self.update_ticket_fields(tkt, update_fields)
[166]827
[236]828                message_parts = self.get_message_parts(m)
[253]829                message_parts = self.unique_attachment_names(message_parts)
[210]830
[309]831                # Must we update some ticket fields properties via body_text
832                #
833                if self.properties:
834                                self.update_ticket_fields(tkt, self.properties)
835
[177]836                if self.EMAIL_HEADER:
[236]837                        message_parts.insert(0, self.email_header_txt(m))
[76]838
[236]839                body_text = self.body_text(message_parts)
840
[348]841                if self.VERSION  == 0.9:
842                        error_with_attachments = self.attach_attachments(message_parts, True)
843                else:
844                        error_with_attachments = self.attach_attachments(message_parts)
845
[309]846                if body_text.strip() or update_fields or self.properties:
[250]847                        if self.DRY_RUN:
[288]848                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]849                        else:
[348]850                                if error_with_attachments:
851                                        body_text = '%s\\%s' %(error_with_attachments, body_text)
852                               
[288]853                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
854                       
[219]855
[204]856                if self.notification and not spam:
[253]857                        self.notify(tkt, False, when)
[72]858
[71]859                return True
860
[202]861        def set_ticket_fields(self, ticket):
[77]862                """
[202]863                set the ticket fields to value specified
864                        - /etc/email2trac.conf with <prefix>_<field>
865                        - trac default values, trac.ini
866                """
867                user_dict = dict()
868
869                for field in ticket.fields:
870
871                        name = field['name']
872
[335]873                        ## default trac value
[202]874                        #
[233]875                        if not field.get('custom'):
876                                value = self.get_config('ticket', 'default_%s' %(name) )
877                        else:
[345]878                                ##  Else we get the default value for reporter
879                                #
[233]880                                value = field.get('value')
881                                options = field.get('options')
[345]882
[335]883                                if value and options and (value not in options):
[345]884                                         value = options[int(value)]
885       
[202]886                        if self.DEBUG > 10:
[379]887                                s = 'TD: trac.ini name %s = %s' %(name, value)
888                                self.print_unicode(s)
[202]889
[335]890                        ## email2trac.conf settings
891                        #
[206]892                        prefix = self.parameters['ticket_prefix']
[202]893                        try:
[206]894                                value = self.parameters['%s_%s' %(prefix, name)]
[202]895                                if self.DEBUG > 10:
[379]896                                        s = 'TD: email2trac.conf %s = %s ' %(name, value)
897                                        self.print_unicode(s)
[202]898
899                        except KeyError, detail:
900                                pass
901               
902                        if self.DEBUG:
[379]903                                s = 'TD: user_dict[%s] = %s' %(name, value)
904                                self.print_unicode(s)
[202]905
[345]906                        if value:
907                                user_dict[name] = value
[202]908
909                self.update_ticket_fields(ticket, user_dict, use_default=1)
910
[352]911                if 'status' not in user_dict.keys():
912                        ticket['status'] = 'new'
[202]913
914
[356]915        def ticket_update_by_subject(self, subject):
916                """
917                This list of Re: prefixes is probably incomplete. Taken from
918                wikipedia. Here is how the subject is matched
919                  - Re: <subject>
920                  - Re: (<Mail list label>:)+ <subject>
[202]921
[356]922                So we must have the last column
923                """
924                if self.VERBOSE:
925                        print "VB: ticket_update_by_subject()"
926
927                matched_id = None
928                if self.TICKET_UPDATE and self.TICKET_UPDATE_BY_SUBJECT:
929                               
930                        SUBJECT_RE = re.compile(r'^(RE|AW|VS|SV):(.*:)*\s*(.*)', re.IGNORECASE)
931                        result = SUBJECT_RE.search(subject)
932
933                        if result:
934                                # This is a reply
935                                orig_subject = result.group(3)
936
937                                if self.DEBUG:
938                                        print 'TD: subject search string: %s' %(orig_subject)
939
940                                cursor = self.db.cursor()
941                                summaries = [orig_subject, '%%: %s' % orig_subject]
942
[360]943                                ##
944                                # Convert days to seconds
945                                lookback = int(time.mktime(time.gmtime())) - \
946                                                self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK * 24 * 3600
[356]947
[360]948
[356]949                                for summary in summaries:
950                                        if self.DEBUG:
951                                                print 'TD: Looking for summary matching: "%s"' % summary
952                                        sql = """SELECT id FROM ticket
953                                                        WHERE changetime >= %s AND summary LIKE %s
954                                                        ORDER BY changetime DESC"""
955                                        cursor.execute(sql, [lookback, summary.strip()])
956
957                                        for row in cursor:
958                                                (matched_id,) = row
959                                                if self.DEBUG:
960                                                        print 'TD: Found matching ticket id: %d' % matched_id
961                                                break
962
963                                        if matched_id:
[366]964                                                matched_id = '#%d' % matched_id
[356]965                                                return matched_id
966
967                return matched_id
968
969
[262]970        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]971                """
[77]972                Create a new ticket
973                """
[356]974                if self.VERBOSE:
975                        print "VB: function new_ticket()"
[250]976
[41]977                tkt = Ticket(self.env)
[326]978
979                self.set_reply_fields(tkt, msg)
980
[202]981                self.set_ticket_fields(tkt)
982
983                # Old style setting for component, will be removed
984                #
[204]985                if spam:
986                        tkt['component'] = 'Spam'
987
[206]988                elif self.parameters.has_key('component'):
989                        tkt['component'] = self.parameters['component']
[201]990
[22]991                if not msg['Subject']:
[151]992                        tkt['summary'] = u'(No subject)'
[22]993                else:
[264]994                        tkt['summary'] = subject
[22]995
996
[262]997                if set_fields:
998                        rest, keywords = string.split(set_fields, '?')
999
1000                        if keywords:
1001                                update_fields = self.str_to_dict(keywords)
1002                                self.update_ticket_fields(tkt, update_fields)
1003
[45]1004                # produce e-mail like header
1005                #
[22]1006                head = ''
1007                if self.EMAIL_HEADER > 0:
1008                        head = self.email_header_txt(msg)
[296]1009
[236]1010                message_parts = self.get_message_parts(msg)
[309]1011
1012                # Must we update some ticket fields properties via body_text
1013                #
1014                if self.properties:
1015                                self.update_ticket_fields(tkt, self.properties)
1016
[296]1017                if self.DEBUG:
1018                        print 'TD: self.get_message_parts ',
1019                        print message_parts
1020
[236]1021                message_parts = self.unique_attachment_names(message_parts)
[296]1022                if self.DEBUG:
1023                        print 'TD: self.unique_attachment_names',
1024                        print message_parts
[236]1025               
1026                if self.EMAIL_HEADER > 0:
1027                        message_parts.insert(0, self.email_header_txt(msg))
1028                       
1029                body_text = self.body_text(message_parts)
[45]1030
[236]1031                tkt['description'] = body_text
[90]1032
[182]1033                #when = int(time.time())
[192]1034                #
[182]1035                utc = UTC()
1036                when = datetime.now(utc)
[45]1037
[253]1038                if not self.DRY_RUN:
1039                        self.id = tkt.insert()
[273]1040       
[90]1041                changed = False
1042                comment = ''
[77]1043
[273]1044                # some routines in trac are dependend on ticket id     
1045                # like alternate notify template
1046                #
1047                if self.notify_template:
[274]1048                        tkt['id'] = self.id
[273]1049                        changed = True
1050
[295]1051                ## Rewrite the description if we have mailto enabled
[45]1052                #
[72]1053                if self.MAILTO:
[100]1054                        changed = True
[142]1055                        comment = u'\nadded mailto line\n'
[343]1056                        mailto = self.html_mailto_link( m['Subject'])
[253]1057
[213]1058                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]1059                                %(head, mailto, body_text)
[295]1060       
1061                ## Save the attachments to the ticket   
1062                #
[340]1063                error_with_attachments =  self.attach_attachments(message_parts)
[295]1064
[319]1065                if error_with_attachments:
1066                        changed = True
1067                        comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1068
[90]1069                if changed:
[204]1070                        if self.DRY_RUN:
[344]1071                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
[201]1072                        else:
[344]1073                                tkt.save_changes(tkt['reporter'], comment)
[201]1074                                #print tkt.get_changelog(self.db, when)
[90]1075
[250]1076                if self.notification and not spam:
[253]1077                        self.notify(tkt, True)
[45]1078
[260]1079
[342]1080        def attach_attachments(self, message_parts, update=False):
1081                '''
1082                save any attachments as files in the ticket's directory
1083                '''
1084                if self.VERBOSE:
1085                        print "VB: attach_attachments()"
1086
1087                if self.DRY_RUN:
1088                        print "DRY_RUN: no attachments attached to tickets"
1089                        return ''
1090
1091                count = 0
1092
1093                # Get Maxium attachment size
1094                #
1095                max_size = int(self.get_config('attachment', 'max_size'))
1096                status   = None
1097               
1098                for item in message_parts:
1099                        # Skip body parts
1100                        if not isinstance(item, tuple):
1101                                continue
1102                               
1103                        (original, filename, part) = item
1104                        #
[377]1105                        # We have to determine the size so we use this temporary solution. we must escape it
1106                        # else we get UnicodeErrors.
[342]1107                        #
[377]1108                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, util.text.unicode_quote(filename)))
[342]1109                        text = part.get_payload(decode=1)
1110                        if not text:
1111                                text = '(None)'
1112                        fd.write(text)
1113                        fd.close()
1114
1115                        # get the file_size
1116                        #
1117                        stats = os.lstat(path)
1118                        file_size = stats[stat.ST_SIZE]
1119
1120                        # Check if the attachment size is allowed
1121                        #
1122                        if (max_size != -1) and (file_size > max_size):
1123                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1124                                        %(status, original, file_size, max_size)
1125
1126                                os.unlink(path)
1127                                continue
1128                        else:
1129                                count = count + 1
1130                                       
1131                        # Insert the attachment
1132                        #
1133                        fd = open(path, 'rb')
[359]1134                        if self.system == 'discussion':
1135                                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1136                                  % (self.id,))
1137                        else:
1138                                att = attachment.Attachment(self.env, 'ticket', self.id)
1139 
[342]1140                        # This will break the ticket_update system, the body_text is vaporized
1141                        # ;-(
1142                        #
1143                        if not update:
1144                                att.author = self.author
1145                                att.description = self.email_to_unicode('Added by email2trac')
1146
[348]1147                        try:
1148                                att.insert(filename, fd, file_size)
1149                        except OSError, detail:
1150                                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
[342]1151
1152                        # Remove the created temporary filename
1153                        #
1154                        fd.close()
1155                        os.unlink(path)
1156
1157                ## return error
1158                #
1159                return status
1160
[359]1161########## Fullblog functions  #################################################
[339]1162
[260]1163        def blog(self, id):
1164                """
1165                The blog create/update function
1166                """
1167                # import the modules
1168                #
1169                from tracfullblog.core import FullBlogCore
[312]1170                from tracfullblog.model import BlogPost, BlogComment
1171                from trac.test import Mock, MockPerm
[260]1172
1173                # instantiate blog core
1174                blog = FullBlogCore(self.env)
[312]1175                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1176
[260]1177                if id:
1178
1179                        # update blog
1180                        #
[268]1181                        comment = BlogComment(self.env, id)
[260]1182                        comment.author = self.author
[312]1183
1184                        message_parts = self.get_message_parts(m)
1185                        comment.comment = self.body_text(message_parts)
1186
[260]1187                        blog.create_comment(req, comment)
1188
1189                else:
1190                        # create blog
1191                        #
1192                        import time
1193                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1194
1195                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1196                       
1197                        post.author = self.author
1198                        post.title = self.email_to_unicode(m['Subject'])
[312]1199
1200                        message_parts = self.get_message_parts(m)
1201                        post.body = self.body_text(message_parts)
[260]1202                       
1203                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1204
1205
[359]1206########## Discussion functions  ##############################################
[342]1207
[359]1208        def discussion_topic(self, content, subject):
[342]1209
[359]1210                # Import modules.
1211                from tracdiscussion.api import DiscussionApi
1212                from trac.util.datefmt import to_timestamp, utc
1213
1214                if self.DEBUG:
1215                        print 'TD: Creating a new topic in forum:', self.id
1216
1217                # Get dissussion API component.
1218                api = self.env[DiscussionApi]
1219                context = self._create_context(content, subject)
1220
1221                # Get forum for new topic.
1222                forum = api.get_forum(context, self.id)
1223
1224                if not forum and self.DEBUG:
1225                        print 'ERROR: Replied forum doesn\'t exist'
1226
1227                # Prepare topic.
1228                topic = {'forum' : forum['id'],
1229                                 'subject' : context.subject,
1230                                 'time': to_timestamp(datetime.now(utc)),
1231                                 'author' : self.author,
1232                                 'subscribers' : [self.email_addr],
1233                                 'body' : self.body_text(context.content_parts)}
1234
1235                # Add topic to DB and commit it.
1236                self._add_topic(api, context, topic)
1237                self.db.commit()
1238
1239        def discussion_topic_reply(self, content, subject):
1240
1241                # Import modules.
1242                from tracdiscussion.api import DiscussionApi
1243                from trac.util.datefmt import to_timestamp, utc
1244
1245                if self.DEBUG:
1246                        print 'TD: Replying to discussion topic', self.id
1247
1248                # Get dissussion API component.
1249                api = self.env[DiscussionApi]
1250                context = self._create_context(content, subject)
1251
1252                # Get replied topic.
1253                topic = api.get_topic(context, self.id)
1254
1255                if not topic and self.DEBUG:
1256                        print 'ERROR: Replied topic doesn\'t exist'
1257
1258                # Prepare message.
1259                message = {'forum' : topic['forum'],
1260                                   'topic' : topic['id'],
1261                                   'replyto' : -1,
1262                                   'time' : to_timestamp(datetime.now(utc)),
1263                                   'author' : self.author,
1264                                   'body' : self.body_text(context.content_parts)}
1265
1266                # Add message to DB and commit it.
1267                self._add_message(api, context, message)
1268                self.db.commit()
1269
1270        def discussion_message_reply(self, content, subject):
1271
1272                # Import modules.
1273                from tracdiscussion.api import DiscussionApi
1274                from trac.util.datefmt import to_timestamp, utc
1275
1276                if self.DEBUG:
1277                        print 'TD: Replying to discussion message', self.id
1278
1279                # Get dissussion API component.
1280                api = self.env[DiscussionApi]
1281                context = self._create_context(content, subject)
1282
1283                # Get replied message.
1284                message = api.get_message(context, self.id)
1285
1286                if not message and self.DEBUG:
1287                        print 'ERROR: Replied message doesn\'t exist'
1288
1289                # Prepare message.
1290                message = {'forum' : message['forum'],
1291                                   'topic' : message['topic'],
1292                                   'replyto' : message['id'],
1293                                   'time' : to_timestamp(datetime.now(utc)),
1294                                   'author' : self.author,
1295                                   'body' : self.body_text(context.content_parts)}
1296
1297                # Add message to DB and commit it.
1298                self._add_message(api, context, message)
1299                self.db.commit()
1300
1301        def _create_context(self, content, subject):
1302
1303                # Import modules.
1304                from trac.mimeview import Context
1305                from trac.web.api import Request
1306                from trac.perm import PermissionCache
1307
1308                # TODO: Read server base URL from config.
1309                # Create request object to mockup context creation.
1310                #
1311                environ = {'SERVER_PORT' : 80,
1312                                   'SERVER_NAME' : 'test',
1313                                   'REQUEST_METHOD' : 'POST',
1314                                   'wsgi.url_scheme' : 'http',
1315                                   'wsgi.input' : sys.stdin}
1316                chrome =  {'links': {},
1317                                   'scripts': [],
1318                                   'ctxtnav': [],
1319                                   'warnings': [],
1320                                   'notices': []}
1321
1322                if self.env.base_url_for_redirect:
1323                        environ['trac.base_url'] = self.env.base_url
1324
1325                req = Request(environ, None)
1326                req.chrome = chrome
1327                req.tz = 'missing'
1328                req.authname = self.author
1329                req.perm = PermissionCache(self.env, self.author)
1330
1331                # Create and return context.
1332                context = Context.from_request(req)
1333                context.realm = 'discussion-email2trac'
1334                context.cursor = self.db.cursor()
1335                context.content = content
1336                context.subject = subject
1337
1338                # Read content parts from content.
1339                context.content_parts = self.get_message_parts(content)
1340                context.content_parts = self.unique_attachment_names(
1341                  context.content_parts)
1342
1343                return context
1344
1345        def _add_topic(self, api, context, topic):
1346                context.req.perm.assert_permission('DISCUSSION_APPEND')
1347
1348                # Filter topic.
1349                for discussion_filter in api.discussion_filters:
1350                        accept, topic_or_error = discussion_filter.filter_topic(
1351                          context, topic)
1352                        if accept:
1353                                topic = topic_or_error
1354                        else:
1355                                raise TracError(topic_or_error)
1356
1357                # Add a new topic.
1358                api.add_topic(context, topic)
1359
1360                # Get inserted topic with new ID.
1361                topic = api.get_topic_by_time(context, topic['time'])
1362
1363                # Attach attachments.
1364                self.id = topic['id']
1365                self.attach_attachments(context.content_parts, self.VERSION == 0.9)
1366
1367                # Notify change listeners.
1368                for listener in api.topic_change_listeners:
1369                        listener.topic_created(context, topic)
1370
1371        def _add_message(self, api, context, message):
1372                context.req.perm.assert_permission('DISCUSSION_APPEND')
1373
1374                # Filter message.
1375                for discussion_filter in api.discussion_filters:
1376                        accept, message_or_error = discussion_filter.filter_message(
1377                          context, message)
1378                        if accept:
1379                                message = message_or_error
1380                        else:
1381                                raise TracError(message_or_error)
1382
1383                # Add message.
1384                api.add_message(context, message)
1385
1386                # Get inserted message with new ID.
1387                message = api.get_message_by_time(context, message['time'])
1388
1389                # Attach attachments.
1390                self.id = message['topic']
1391                self.attach_attachments(context.content_parts, self.VERSION == 0.9)
1392
1393                # Notify change listeners.
1394                for listener in api.message_change_listeners:
1395                        listener.message_created(context, message)
1396
1397########## MAIN function  ######################################################
1398
[77]1399        def parse(self, fp):
[356]1400                """
1401                """
1402                if self.VERBOSE:
1403                        print "VB: main function parse()"
[96]1404                global m
1405
[77]1406                m = email.message_from_file(fp)
[239]1407               
[77]1408                if not m:
[221]1409                        if self.DEBUG:
[250]1410                                print "TD: This is not a valid email message format"
[77]1411                        return
[239]1412                       
1413                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
[316]1414                try:
1415                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1416                except AttributeError, detail:
1417                        pass
[239]1418
[77]1419                if self.DEBUG > 1:        # save the entire e-mail message text
[219]1420                        self.save_email_for_debug(m, True)
[77]1421
1422                self.db = self.env.get_db_cnx()
[194]1423                self.get_sender_info(m)
[152]1424
[221]1425                if not self.email_header_acl('white_list', self.email_addr, True):
1426                        if self.DEBUG > 1 :
1427                                print 'Message rejected : %s not in white list' %(self.email_addr)
1428                        return False
[77]1429
[221]1430                if self.email_header_acl('black_list', self.email_addr, False):
1431                        if self.DEBUG > 1 :
1432                                print 'Message rejected : %s in black list' %(self.email_addr)
1433                        return False
1434
[227]1435                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]1436                        if self.DEBUG > 1 :
1437                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1438                        return False
1439
[204]1440                # If drop the message
[194]1441                #
[204]1442                if self.spam(m) == 'drop':
[194]1443                        return False
1444
[204]1445                elif self.spam(m) == 'spam':
1446                        spam_msg = True
1447                else:
1448                        spam_msg = False
1449
[77]1450                if self.get_config('notification', 'smtp_enabled') in ['true']:
1451                        self.notification = 1
1452                else:
1453                        self.notification = 0
1454
[359]1455                if not m['Subject']:
1456                        subject  = 'No Subject'
1457                else:
1458                        subject  = self.email_to_unicode(m['Subject'])
[304]1459
[359]1460                if self.DEBUG:
1461                         print "TD:", subject
1462
1463                #
1464                # [hic] #1529: Re: LRZ
1465                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1466                #
1467                ticket_regex = r'''
1468                        (?P<new_fields>[#][?].*)
1469                        |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*?:)*)
1470                        '''
[260]1471                # Check if  FullBlogPlugin is installed
[77]1472                #
[260]1473                blog_enabled = None
[359]1474                blog_regex = ''
[260]1475                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1476                        blog_enabled = True
[359]1477                        blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1478
[77]1479
[359]1480                # Check if DiscussionPlugin is installed
[260]1481                #
[359]1482                discussion_enabled = None
1483                discussion_regex = ''
1484                if self.get_config('components', 'tracdiscussion.api.*') in ['enabled']:
1485                        discussion_enabled = True
1486                        discussion_regex = r'''
1487                        |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1488                        |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1489                        |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1490                        '''
[77]1491
[359]1492
1493                regex_str = ticket_regex + blog_regex + discussion_regex
1494                SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1495
1496                # Find out if this is a ticket, a blog or a discussion
[265]1497                #
[359]1498                result =  SYSTEM_RE.search(subject)
[260]1499                if result:
1500                        # update ticket + fields
1501                        #
[359]1502                        if result.group('reply') and self.TICKET_UPDATE:
1503                                self.system = 'ticket'
[262]1504                                self.ticket_update(m, result.group('reply'), spam_msg)
[260]1505
[262]1506                        # New ticket + fields
1507                        #
1508                        elif result.group('new_fields'):
[359]1509                                self.system = 'ticket'
[262]1510                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1511
[359]1512                        if blog_enabled:
1513                                if result.group('blog'):
1514                                        self.system = 'blog'
1515                                        self.blog(result.group('blog_id'))
1516
1517                        if discussion_enabled:
1518                                # New topic.
1519                                #
1520                                if result.group('forum'):
1521                                        self.system = 'discussion'
1522                                        self.id = int(result.group('forum_id'))
1523                                        self.discussion_topic(m, subject[result.end('forum'):])
1524
1525                                # Reply to topic.
1526                                #
1527                                elif result.group('topic'):
1528                                        self.system = 'discussion'
1529                                        self.id = int(result.group('topic_id'))
1530                                        self.discussion_topic_reply(m, subject[result.end('topic'):])
1531
1532                                # Reply to topic message.
1533                                #
1534                                elif result.group('message'):
1535                                        self.system = 'discussion'
1536                                        self.id = int(result.group('message_id'))
1537                                        self.discussion_message_reply(m, subject[result.end('message'):])
1538
[260]1539                else:
[359]1540                        self.system = 'ticket'
[356]1541                        result = self.ticket_update_by_subject(subject)
1542                        if result:
1543                                self.ticket_update(m, result, spam_msg)
[353]1544                        else:
1545                                # No update by subject, so just create a new ticket
1546                                self.new_ticket(m, subject, spam_msg)
1547
[356]1548
[343]1549########## BODY TEXT functions  ###########################################################
1550
[136]1551        def strip_signature(self, text):
1552                """
1553                Strip signature from message, inspired by Mailman software
1554                """
1555                body = []
1556                for line in text.splitlines():
1557                        if line == '-- ':
1558                                break
1559                        body.append(line)
1560
1561                return ('\n'.join(body))
1562
[231]1563        def reflow(self, text, delsp = 0):
1564                """
1565                Reflow the message based on the format="flowed" specification (RFC 3676)
1566                """
1567                flowedlines = []
1568                quotelevel = 0
1569                prevflowed = 0
1570
1571                for line in text.splitlines():
1572                        from re import match
1573                       
1574                        # Figure out the quote level and the content of the current line
1575                        m = match('(>*)( ?)(.*)', line)
1576                        linequotelevel = len(m.group(1))
1577                        line = m.group(3)
1578
1579                        # Determine whether this line is flowed
1580                        if line and line != '-- ' and line[-1] == ' ':
1581                                flowed = 1
1582                        else:
1583                                flowed = 0
1584
1585                        if flowed and delsp and line and line[-1] == ' ':
1586                                line = line[:-1]
1587
1588                        # If the previous line is flowed, append this line to it
1589                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1590                                flowedlines[-1] += line
1591                        # Otherwise, start a new line
1592                        else:
1593                                flowedlines.append('>' * linequotelevel + line)
1594
1595                        prevflowed = flowed
1596                       
1597
1598                return '\n'.join(flowedlines)
1599
[191]1600        def strip_quotes(self, text):
[193]1601                """
1602                Strip quotes from message by Nicolas Mendoza
1603                """
1604                body = []
1605                for line in text.splitlines():
1606                        if line.startswith(self.EMAIL_QUOTE):
1607                                continue
1608                        body.append(line)
[151]1609
[193]1610                return ('\n'.join(body))
[191]1611
[309]1612        def inline_properties(self, text):
1613                """
1614                Parse text if we use inline keywords to set ticket fields
1615                """
1616                if self.DEBUG:
1617                        print 'TD: inline_properties function'
1618
1619                properties = dict()
1620                body = list()
1621
1622                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1623
1624                for line in text.splitlines():
1625                        match = INLINE_EXP.match(line)
1626                        if match:
1627                                keyword, value = match.groups()
1628                                self.properties[keyword] = value.strip()
[311]1629                                if self.DEBUG:
1630                                        print "TD: inline properties: %s : %s" %(keyword,value)
[309]1631                        else:
1632                                body.append(line)
1633                               
1634                return '\n'.join(body)
1635
1636
[154]1637        def wrap_text(self, text, replace_whitespace = False):
[151]1638                """
[191]1639                Will break a lines longer then given length into several small
1640                lines of size given length
[151]1641                """
1642                import textwrap
[154]1643
[151]1644                LINESEPARATOR = '\n'
[153]1645                reformat = ''
[151]1646
[154]1647                for s in text.split(LINESEPARATOR):
1648                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1649                        if tmp:
1650                                reformat = '%s\n%s' %(reformat,tmp)
1651                        else:
1652                                reformat = '%s\n' %reformat
[153]1653
1654                return reformat
1655
[154]1656                # Python2.4 and higher
1657                #
1658                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1659                #
1660
[343]1661########## EMAIL attachements functions ###########################################################
1662
[340]1663        def inline_part(self, part):
1664                """
1665                """
1666                if self.VERBOSE:
1667                        print "VB: inline_part()"
[154]1668
[340]1669                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1670
[236]1671        def get_message_parts(self, msg):
[45]1672                """
[236]1673                parses the email message and returns a list of body parts and attachments
1674                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1675                """
[317]1676                if self.VERBOSE:
1677                        print "VB: get_message_parts()"
1678
[309]1679                message_parts = list()
[294]1680       
[278]1681                ALTERNATIVE_MULTIPART = False
1682
[22]1683                for part in msg.walk():
[236]1684                        if self.DEBUG:
[278]1685                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
[236]1686                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[278]1687
1688                        ## Check content type
[294]1689                        #
1690                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[238]1691
[294]1692                                if self.DEBUG:
1693                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
[238]1694
1695                                continue
1696
[294]1697                        ## Catch some mulitpart execptions
1698                        #
1699                        if part.get_content_type() == 'multipart/alternative':
[278]1700                                ALTERNATIVE_MULTIPART = True
1701                                continue
1702
[294]1703                        ## Skip multipart containers
[278]1704                        #
[45]1705                        if part.get_content_maintype() == 'multipart':
[278]1706                                if self.DEBUG:
1707                                        print "TD: Skipping multipart container"
[22]1708                                continue
[278]1709                       
[294]1710                        ## 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"
1711                        #
[236]1712                        inline = self.inline_part(part)
1713
[294]1714                        ## Drop HTML message
1715                        #
[278]1716                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1717                                if part.get_content_type() == 'text/html':
1718                                        if self.DEBUG:
1719                                                print "TD: Skipping alternative HTML message"
1720
1721                                        ALTERNATIVE_MULTIPART = False
1722                                        continue
1723
[294]1724                        ## Inline text parts are where the body is
1725                        #
[236]1726                        if part.get_content_type() == 'text/plain' and inline:
1727                                if self.DEBUG:
1728                                        print 'TD:               Inline body part'
1729
[45]1730                                # Try to decode, if fails then do not decode
1731                                #
[90]1732                                body_text = part.get_payload(decode=1)
[45]1733                                if not body_text:                       
[90]1734                                        body_text = part.get_payload(decode=0)
[231]1735
[232]1736                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1737                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1738
1739                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1740                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1741       
[136]1742                                if self.STRIP_SIGNATURE:
1743                                        body_text = self.strip_signature(body_text)
[22]1744
[191]1745                                if self.STRIP_QUOTES:
1746                                        body_text = self.strip_quotes(body_text)
1747
[309]1748                                if self.INLINE_PROPERTIES:
1749                                        body_text = self.inline_properties(body_text)
1750
[148]1751                                if self.USE_TEXTWRAP:
[151]1752                                        body_text = self.wrap_text(body_text)
[148]1753
[294]1754                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1755                                #
[100]1756                                charset = part.get_content_charset()
[102]1757                                if not charset:
1758                                        charset = 'iso-8859-15'
1759
[89]1760                                try:
[96]1761                                        ubody_text = unicode(body_text, charset)
[100]1762
1763                                except UnicodeError, detail:
[96]1764                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1765
[100]1766                                except LookupError, detail:
[139]1767                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1768
[236]1769                                if self.VERBATIM_FORMAT:
1770                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1771                                else:
1772                                        message_parts.append('%s' %ubody_text)
1773                        else:
1774                                if self.DEBUG:
[379]1775                                        s = 'TD:               Filename: %s' % part.get_filename()
1776                                        self.print_unicode(s)
[22]1777
[383]1778                                ##
1779                                #  First try to use email header function to convert filename.
1780                                #  If this fails the use the plan filename
1781                                try:
1782                                        filename = self.email_to_unicode(part.get_filename())
1783                                except UnicodeEncodeError, detail:
1784                                        filename = part.get_filename()
1785
[317]1786                                message_parts.append((filename, part))
[236]1787
1788                return message_parts
1789               
[253]1790        def unique_attachment_names(self, message_parts):
[296]1791                """
1792                """
[236]1793                renamed_parts = []
1794                attachment_names = set()
[296]1795
[331]1796                for item in message_parts:
[236]1797                       
[296]1798                        ## If not an attachment, leave it alone
1799                        #
[331]1800                        if not isinstance(item, tuple):
1801                                renamed_parts.append(item)
[236]1802                                continue
1803                               
[331]1804                        (filename, part) = item
[295]1805
[296]1806                        ## If no filename, use a default one
1807                        #
1808                        if not filename:
[236]1809                                filename = 'untitled-part'
[22]1810
[242]1811                                # Guess the extension from the content type, use non strict mode
1812                                # some additional non-standard but commonly used MIME types
1813                                # are also recognized
1814                                #
1815                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1816                                if not ext:
1817                                        ext = '.bin'
[22]1818
[236]1819                                filename = '%s%s' % (filename, ext)
[22]1820
[348]1821                        ## Discard relative paths for windows/unix in attachment names
[296]1822                        #
[348]1823                        #filename = filename.replace('\\', '/').replace(':', '/')
1824                        filename = filename.replace('\\', '_')
1825                        filename = filename.replace('/', '_')
[347]1826
[296]1827                        #
[236]1828                        # We try to normalize the filename to utf-8 NFC if we can.
1829                        # Files uploaded from OS X might be in NFD.
1830                        # Check python version and then try it
1831                        #
[348]1832                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1833                        #       try:
1834                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1835                        #       except TypeError:
1836                        #               pass
[100]1837
[236]1838                        # Make the filename unique for this ticket
1839                        num = 0
1840                        unique_filename = filename
[296]1841                        dummy_filename, ext = os.path.splitext(filename)
[134]1842
[339]1843                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
[236]1844                                num += 1
[296]1845                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]1846                               
1847                        if self.DEBUG:
[379]1848                                s = 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1849                                self.print_unicode(s)
[100]1850
[236]1851                        attachment_names.add(unique_filename)
1852
1853                        renamed_parts.append((filename, unique_filename, part))
[296]1854       
[236]1855                return renamed_parts
1856                       
1857                       
[253]1858        def attachment_exists(self, filename):
[250]1859
1860                if self.DEBUG:
[379]1861                        s = 'TD: attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
1862                        self.print_unicode(s)
[250]1863
1864                # We have no valid ticket id
1865                #
[253]1866                if not self.id:
[236]1867                        return False
[250]1868
[236]1869                try:
[359]1870                        if self.system == 'discussion':
1871                                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
1872                                  % (self.id,), filename)
1873                        else:
1874                                att = attachment.Attachment(self.env, 'ticket', self.id,
1875                                  filename)
[236]1876                        return True
[250]1877                except attachment.ResourceNotFound:
[236]1878                        return False
[343]1879
1880########## TRAC Ticket Text ###########################################################
[236]1881                       
1882        def body_text(self, message_parts):
1883                body_text = []
1884               
1885                for part in message_parts:
1886                        # Plain text part, append it
1887                        if not isinstance(part, tuple):
1888                                body_text.extend(part.strip().splitlines())
1889                                body_text.append("")
1890                                continue
1891                               
1892                        (original, filename, part) = part
1893                        inline = self.inline_part(part)
1894                       
1895                        if part.get_content_maintype() == 'image' and inline:
[359]1896                                if self.system != 'discussion':
1897                                        body_text.append('[[Image(%s)]]' % filename)
[236]1898                                body_text.append("")
1899                        else:
[359]1900                                if self.system != 'discussion':
1901                                        body_text.append('[attachment:"%s"]' % filename)
[236]1902                                body_text.append("")
1903                               
1904                body_text = '\r\n'.join(body_text)
1905                return body_text
1906
[343]1907        def html_mailto_link(self, subject):
1908                """
1909                This function returns a HTML mailto tag with the ticket id and author email address
1910                """
1911                if not self.author:
1912                        author = self.email_addr
1913                else:   
1914                        author = self.author
1915
1916                # use urllib to escape the chars
1917                #
1918                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1919                       urllib.quote(self.email_addr),
1920                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1921                           urllib.quote(self.MAILTO_CC)
1922                           )
1923
1924                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)
1925                return s
1926
1927########## TRAC notify section ###########################################################
1928
[253]1929        def notify(self, tkt, new=True, modtime=0):
[79]1930                """
1931                A wrapper for the TRAC notify function. So we can use templates
1932                """
[250]1933                if self.DRY_RUN:
[344]1934                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
[250]1935                                return
[41]1936                try:
1937                        # create false {abs_}href properties, to trick Notify()
1938                        #
[369]1939                        if not (self.VERSION in [0.11, 0.12]):
[192]1940                                self.env.abs_href = Href(self.get_config('project', 'url'))
1941                                self.env.href = Href(self.get_config('project', 'url'))
[22]1942
[41]1943                        tn = TicketNotifyEmail(self.env)
[213]1944
[42]1945                        if self.notify_template:
[222]1946
[388]1947                                if self.VERSION >= 0.11:
[222]1948
[221]1949                                        from trac.web.chrome import Chrome
[222]1950
1951                                        if self.notify_template_update and not new:
1952                                                tn.template_name = self.notify_template_update
1953                                        else:
1954                                                tn.template_name = self.notify_template
1955
[221]1956                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1957                                               
1958                                else:
[222]1959
[221]1960                                        tn.template_name = self.notify_template;
[42]1961
[77]1962                        tn.notify(tkt, new, modtime)
[41]1963
1964                except Exception, e:
[253]1965                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
[41]1966
[22]1967
[74]1968
[343]1969########## Parse Config File  ###########################################################
[22]1970
1971def ReadConfig(file, name):
1972        """
1973        Parse the config file
1974        """
1975        if not os.path.isfile(file):
[79]1976                print 'File %s does not exist' %file
[22]1977                sys.exit(1)
1978
[199]1979        config = trac_config.Configuration(file)
[22]1980
1981        # Use given project name else use defaults
1982        #
1983        if name:
[199]1984                sections = config.sections()
1985                if not name in sections:
[79]1986                        print "Not a valid project name: %s" %name
[199]1987                        print "Valid names: %s" %sections
[22]1988                        sys.exit(1)
1989
1990                project =  dict()
[199]1991                for option, value in  config.options(name):
1992                        project[option] = value
[22]1993
1994        else:
[270]1995                # use some trac internals to get the defaults
[217]1996                #
1997                project = config.parser.defaults()
[22]1998
1999        return project
2000
[87]2001
[22]2002if __name__ == '__main__':
2003        # Default config file
2004        #
[24]2005        configfile = '@email2trac_conf@'
[22]2006        project = ''
2007        component = ''
[202]2008        ticket_prefix = 'default'
[204]2009        dry_run = None
[317]2010        verbose = None
[202]2011
[87]2012        ENABLE_SYSLOG = 0
[201]2013
[317]2014        SHORT_OPT = 'chf:np:t:v'
2015        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2016
[22]2017        try:
[201]2018                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]2019        except getopt.error,detail:
2020                print __doc__
2021                print detail
2022                sys.exit(1)
[87]2023       
[22]2024        project_name = None
2025        for opt,value in opts:
2026                if opt in [ '-h', '--help']:
2027                        print __doc__
2028                        sys.exit(0)
2029                elif opt in ['-c', '--component']:
2030                        component = value
2031                elif opt in ['-f', '--file']:
2032                        configfile = value
[201]2033                elif opt in ['-n', '--dry-run']:
[204]2034                        dry_run = True
[22]2035                elif opt in ['-p', '--project']:
2036                        project_name = value
[202]2037                elif opt in ['-t', '--ticket_prefix']:
2038                        ticket_prefix = value
[388]2039                elif opt in ['-v', '--verbose']:
[317]2040                        verbose = True
[87]2041       
[22]2042        settings = ReadConfig(configfile, project_name)
2043        if not settings.has_key('project'):
2044                print __doc__
[79]2045                print 'No Trac project is defined in the email2trac config file.'
[22]2046                sys.exit(1)
[87]2047       
[22]2048        if component:
2049                settings['component'] = component
[202]2050
2051        # The default prefix for ticket values in email2trac.conf
2052        #
2053        settings['ticket_prefix'] = ticket_prefix
[206]2054        settings['dry_run'] = dry_run
[317]2055        settings['verbose'] = verbose
[22]2056
[189]2057
[363]2058        # Determine major trac version used to be in email2trac.conf
[373]2059        # Quick hack for 0.12
[363]2060        #
2061        version = '0.%s' %(trac_version.split('.')[1])
[373]2062        if version.startswith('0.12'):
2063                version = '0.12'
2064
[363]2065        if verbose:
2066                print "Found trac version: %s" %(version)
2067       
[22]2068        #debug HvB
2069        #print settings
[189]2070
[87]2071        try:
[189]2072                if version == '0.9':
[87]2073                        from trac import attachment
2074                        from trac.env import Environment
2075                        from trac.ticket import Ticket
2076                        from trac.web.href import Href
2077                        from trac import util
2078                        from trac.Notify import TicketNotifyEmail
[189]2079                elif version == '0.10':
[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
[139]2085                        #
2086                        # return  util.text.to_unicode(str)
2087                        #
[87]2088                        # see http://projects.edgewall.com/trac/changeset/2799
2089                        from trac.ticket.notification import TicketNotifyEmail
[199]2090                        from trac import config as trac_config
[359]2091                        from trac.core import TracError
2092
[189]2093                elif version == '0.11':
[182]2094                        from trac import attachment
2095                        from trac.env import Environment
2096                        from trac.ticket import Ticket
2097                        from trac.web.href import Href
[199]2098                        from trac import config as trac_config
[182]2099                        from trac import util
[359]2100                        from trac.core import TracError
[260]2101
[368]2102                        #
2103                        # return  util.text.to_unicode(str)
2104                        #
2105                        # see http://projects.edgewall.com/trac/changeset/2799
2106                        from trac.ticket.notification import TicketNotifyEmail
[260]2107
[368]2108                elif version == '0.12':
2109                        from trac import attachment
2110                        from trac.env import Environment
2111                        from trac.ticket import Ticket
2112                        from trac.web.href import Href
2113                        from trac import config as trac_config
2114                        from trac import util
2115                        from trac.core import TracError
2116
[182]2117                        #
2118                        # return  util.text.to_unicode(str)
2119                        #
2120                        # see http://projects.edgewall.com/trac/changeset/2799
2121                        from trac.ticket.notification import TicketNotifyEmail
[368]2122
2123
[189]2124                else:
2125                        print 'TRAC version %s is not supported' %version
2126                        sys.exit(1)
2127                       
2128                if settings.has_key('enable_syslog'):
[190]2129                        if SYSLOG_AVAILABLE:
2130                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]2131
[291]2132
2133                # Must be set before environment is created
2134                #
2135                if settings.has_key('python_egg_cache'):
2136                        python_egg_cache = str(settings['python_egg_cache'])
2137                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2138
[359]2139       
2140                if int(settings['debug']) > 0:
2141                        print 'Loading environment', settings['project']
2142
[87]2143                env = Environment(settings['project'], create=0)
[333]2144
[206]2145                tktparser = TicketEmailParser(env, settings, float(version))
[87]2146                tktparser.parse(sys.stdin)
[22]2147
[87]2148        # Catch all errors ans log to SYSLOG if we have enabled this
2149        # else stdout
2150        #
2151        except Exception, error:
2152                if ENABLE_SYSLOG:
2153                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]2154
[87]2155                        etype, evalue, etb = sys.exc_info()
2156                        for e in traceback.format_exception(etype, evalue, etb):
2157                                syslog.syslog(e)
[187]2158
[87]2159                        syslog.closelog()
2160                else:
2161                        traceback.print_exc()
[22]2162
[97]2163                if m:
[98]2164                        tktparser.save_email_for_debug(m, True)
[97]2165
[356]2166
[249]2167                sys.exit(1)
[22]2168# EOB
Note: See TracBrowser for help on using the repository browser.