source: trunk/email2trac.py.in @ 532

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

fix a bug in the windows logging environment, closes #251

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