source: trunk/email2trac.py.in @ 420

Last change on this file since 420 was 420, checked in by bas, 12 years ago

layout fixes

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