source: trunk/email2trac.py.in @ 507

Last change on this file since 507 was 507, checked in by bas, 13 years ago

converted tabs to spaces

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