source: trunk/email2trac.py.in @ 428

Last change on this file since 428 was 428, checked in by bas, 11 years ago

email2trac.py.in:

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