source: trunk/email2trac.py.in @ 431

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

Fixed another spellimg ;-) error set.logger. must be self.logger, closes #212

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