source: trunk/email2trac.py.in @ 529

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

fixed a bug in ticket update by subject for trac version less then 0.12, closes #248

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 79.8 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 529 2011-06-30 08:42:57Z 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        for field in ticket.fields:
736            try:
737                sys_dict[field['name']] = field['options']
738
739            except KeyError:
740                #sys_dict[field['name']] = None
741                pass
742
743        ## Check user supplied fields an compare them with the
744        #  system one's
745        #
746        for field,value in user_dict.items():
747
748            if self.parameters.debug:
749                s = 'user_field\t %s = %s' %(field,value)
750                self.print_unicode(s)
751
752            if not field in ticket.fields: 
753                self.logger.debug('%s is not a valid field for tickets' %(field))
754                continue
755
756            ## To prevent mail loop
757            #
758            if field == 'cc':
759
760                cc_list = user_dict['cc'].split(',')
761
762                if self.trac_smtp_from in cc_list:
763                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
764
765                    cc_list.remove(self.trac_smtp_from)
766
767                value = ','.join(cc_list)
768               
769
770            ## Check if every value is allowed for this field
771            #
772            if sys_dict.has_key(field):
773
774                if value in sys_dict[field]:
775                    ticket[field] = value
776                else:
777                    ## Must we set a default if value is not allowed
778                    #
779                    if new:
780                        value = self.get_config('ticket', 'default_%s' %(field) )
781
782            else:
783                ## Only set if we have a value
784                #
785                #if value:
786                #   ticket[field] = value
787                ticket[field] = value
788
789            if self.parameters.debug:
790                s = 'ticket_field\t %s = %s' %(field,  ticket[field])
791                self.print_unicode(s)
792
793    def ticket_update(self, m, id, spam):
794        """
795        If the current email is a reply to an existing ticket, this function
796        will append the contents of this email to that ticket, instead of
797        creating a new one.
798        """
799        self.logger.debug("function ticket_update")
800
801        if not self.parameters.ticket_update:
802            self.logger.debug("ticket_update disabled")
803            return False
804
805        ## Must we update ticket fields
806        #
807        update_fields = dict()
808        try:
809            id, keywords = string.split(id, '?')
810
811            update_fields = self.str_to_dict(keywords)
812
813            ## Strip '#'
814            #
815            self.id = int(id[1:])
816
817        except ValueError:
818
819            ## Strip '#'
820            #
821            self.id = int(id[1:])
822
823        self.logger.debug("ticket_update id %s" %id)
824
825        ## When is the change committed
826        #
827        if self.VERSION < 0.11:
828            when = int(time.time())
829        else:
830            when = datetime.now(util.datefmt.utc)
831
832        try:
833            tkt = Ticket(self.env, self.id, self.db)
834
835        except util.TracError, detail:
836
837            ## Not a valid ticket
838            #
839            self.id = None
840            return False
841
842        ## Check the permission of the reporter
843        #
844        if self.parameters.ticket_permission_system:
845            if not self.check_permission(tkt, 'TICKET_APPEND'):
846                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
847                return False
848
849        ## How many changes has this ticket
850        #
851        # cnum = len(tkt.get_changelog())
852        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
853        cnum = sum(1 for e in grouped) + 1
854
855
856        ## reopen the ticket if it is was closed
857        #  We must use the ticket workflow framework
858        #
859        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
860
861            self.logger.debug('Workflow ticket update fields: ')
862
863            from trac.ticket.default_workflow import ConfigurableTicketWorkflow
864            from trac.test import Mock, MockPerm
865
866            req = Mock(authname=self.author, perm=MockPerm(), args={})
867            try:
868                workflow = self.parameters['workflow_%s' %tkt['status']]
869            except KeyError:
870                ##
871                # fallback for compability (Will be deprecated)
872                # workflow can be none.
873                #
874                workflow = None
875                if tkt['status'] in ['closed']:
876                    workflow = self.parameters.workflow
877
878            controller = ConfigurableTicketWorkflow(self.env)
879            #print controller.actions
880            #print controller.actions.keys()
881            #print controller.get_ticket_actions(req, tkt)
882            #print controller.actions[workflow]
883            #print controller.actions[workflow]['permissions'] is a list
884
885            if workflow:
886
887                if self.parameters.ticket_permission_system:
888
889                    if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
890                        fields = controller.get_ticket_changes(req, tkt, workflow)
891                    else:
892                        fields = dict()
893                        self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
894
895                else:
896                    fields = controller.get_ticket_changes(req, tkt, workflow)
897
898                for key in fields.keys():
899                    self.logger.debug('\t %s : %s' %(key, fields[key]))
900                    tkt[key] = fields[key]
901
902            else:
903
904                if tkt['status'] in ['closed']:
905                    tkt['status'] = 'reopened'
906                    tkt['resolution'] = ''
907
908        ## Old pre 0.11 situation
909        #
910        elif self.parameters.email_triggers_workflow:
911
912            self.logger.debug('email triggers workflow pre trac 0.11')
913
914            if tkt['status'] in ['closed']:
915                tkt['status'] = 'reopened'
916                tkt['resolution'] = ''
917
918        else:
919            self.logger.debug('email triggers workflow disabled')
920
921        ## Must we update some ticket fields properties via subject line
922        #
923        if update_fields:
924            self.update_ticket_fields(tkt, update_fields)
925
926        message_parts = self.get_message_parts(m)
927        message_parts = self.unique_attachment_names(message_parts)
928
929        ## Must we update some ticket fields properties via body_text
930        #
931        if self.properties:
932                self.update_ticket_fields(tkt, self.properties)
933
934        if self.parameters.email_header:
935            message_parts.insert(0, self.email_header_txt(m))
936
937        body_text = self.get_body_text(message_parts)
938
939        error_with_attachments = self.attach_attachments(message_parts)
940
941        if body_text.strip() or update_fields or self.properties:
942            if self.parameters.dry_run:
943                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
944            else:
945                if error_with_attachments:
946                    body_text = '%s\\%s' %(error_with_attachments, body_text)
947                self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum))
948                tkt.save_changes(self.author, body_text, when, None, str(cnum))
949           
950
951        if not spam:
952            self.notify(tkt, False, when)
953
954        return True
955
956    def set_ticket_fields(self, ticket):
957        """
958        set the ticket fields to value specified
959            - /etc/email2trac.conf with <prefix>_<field>
960            - trac default values, trac.ini
961        """
962        self.logger.debug('function set_ticket_fields')
963
964        user_dict = dict()
965
966        for field in ticket.fields:
967
968            name = field['name']
969
970            ## default trac value
971            #
972            if not field.get('custom'):
973                value = self.get_config('ticket', 'default_%s' %(name) )
974                if (name in ['resolution']) and (value in ['fixed']):
975                    value = None
976            else:
977                ##  Else get the default value for reporter
978                #
979                value = field.get('value')
980                options = field.get('options')
981
982                if value and options and (value not in options):
983                     value = options[int(value)]
984   
985            if self.parameters.debug:
986                s = 'trac[%s] = %s' %(name, value)
987                self.print_unicode(s)
988
989            ## email2trac.conf settings
990            #
991            prefix = self.parameters.ticket_prefix
992            try:
993                value = self.parameters['%s_%s' %(prefix, name)]
994                if self.parameters.debug:
995                    s = 'email2trac[%s] = %s ' %(name, value)
996                    self.print_unicode(s)
997
998            except KeyError, detail:
999                pass
1000       
1001            if value:
1002                user_dict[name] = value
1003                if self.parameters.debug:
1004                    s = 'used %s = %s' %(name, value)
1005                    self.print_unicode(s)
1006
1007        self.update_ticket_fields(ticket, user_dict, new=1)
1008
1009        if 'status' not in user_dict.keys():
1010            ticket['status'] = 'new'
1011
1012
1013    def ticket_update_by_subject(self, subject):
1014        """
1015        This list of Re: prefixes is probably incomplete. Taken from
1016        wikipedia. Here is how the subject is matched
1017          - Re: <subject>
1018          - Re: (<Mail list label>:)+ <subject>
1019
1020        So we must have the last column
1021        """
1022        self.logger.debug('function ticket_update_by_subject')
1023
1024        matched_id = None
1025        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
1026               
1027            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
1028            result = SUBJECT_RE.search(subject)
1029
1030            if result:
1031                ## This is a reply
1032                #
1033                orig_subject = result.group(1)
1034
1035                self.logger.debug('subject search string: %s' %(orig_subject))
1036
1037                cursor = self.db.cursor()
1038                summaries = [orig_subject, '%%: %s' % orig_subject]
1039
1040                ## Time resolution is in micoseconds
1041                #
1042                search_date = datetime.now(util.datefmt.utc) - timedelta(days=self.parameters.ticket_update_by_subject_lookback)
1043
1044                if self.VERSION < 0.12:
1045                    lookback = util.datefmt.to_timestamp(search_date)
1046                else:
1047                    lookback = util.datefmt.to_utimestamp(search_date)
1048
1049                for summary in summaries:
1050                    self.logger.debug('Looking for summary matching: "%s"' % summary)
1051
1052                    sql = """SELECT id FROM ticket
1053                            WHERE changetime >= %s AND summary LIKE %s
1054                            ORDER BY changetime DESC"""
1055                    cursor.execute(sql, [lookback, summary.strip()])
1056
1057                    for row in cursor:
1058                        (matched_id,) = row
1059
1060                        self.logger.debug('Found matching ticket id: %d' % matched_id)
1061
1062                        break
1063
1064                    if matched_id:
1065                        matched_id = '#%d' % matched_id
1066                        return (matched_id, orig_subject)
1067                   
1068                    subject = orig_subject
1069
1070        return (matched_id, subject)
1071
1072
1073    def new_ticket(self, msg, subject, spam, set_fields = None):
1074        """
1075        Create a new ticket
1076        """
1077        self.logger.debug('function new_ticket')
1078
1079        tkt = Ticket(self.env)
1080
1081        self.set_reply_fields(tkt, msg)
1082
1083        self.set_ticket_fields(tkt)
1084
1085        ## Check the permission of the reporter
1086        #
1087        if self.parameters.ticket_permission_system:
1088            if not self.check_permission(tkt, 'TICKET_CREATE'):
1089                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1090                return False
1091
1092        ## Old style setting for component, will be removed
1093        #
1094        if spam:
1095            tkt['component'] = 'Spam'
1096
1097        elif self.parameters.has_key('component'):
1098            tkt['component'] = self.parameters['component']
1099
1100        if not msg['Subject']:
1101            tkt['summary'] = u'(No subject)'
1102        else:
1103            tkt['summary'] = subject
1104
1105
1106        if set_fields:
1107            rest, keywords = string.split(set_fields, '?')
1108
1109            if keywords:
1110                update_fields = self.str_to_dict(keywords)
1111                self.update_ticket_fields(tkt, update_fields)
1112
1113
1114        message_parts = self.get_message_parts(msg)
1115
1116        ## Must we update some ticket fields properties via body_text
1117        #
1118        if self.properties:
1119                self.update_ticket_fields(tkt, self.properties)
1120
1121        message_parts = self.unique_attachment_names(message_parts)
1122       
1123        ## produce e-mail like header
1124        #
1125        head = ''
1126        if self.parameters.email_header:
1127            head = self.email_header_txt(msg)
1128            message_parts.insert(0, head)
1129           
1130        body_text = self.get_body_text(message_parts)
1131
1132        tkt['description'] = body_text
1133
1134        ## When is the change committed
1135        #
1136        if self.VERSION < 0.11:
1137            when = int(time.time())
1138        else:
1139            when = datetime.now(util.datefmt.utc)
1140
1141        if self.parameters.dry_run:
1142            print 'DRY_RUN: tkt.insert()'
1143        else:
1144            self.id = tkt.insert()
1145   
1146        changed = False
1147        comment = ''
1148
1149        ## some routines in trac are dependend on ticket id
1150        #  like alternate notify template
1151        #
1152        if self.parameters.alternate_notify_template:
1153            tkt['id'] = self.id
1154            changed = True
1155
1156        ## Rewrite the description if we have mailto enabled
1157        #
1158        if self.parameters.mailto_link:
1159            changed = True
1160            comment = u'\nadded mailto line\n'
1161            mailto = self.html_mailto_link( m['Subject'])
1162
1163            tkt['description'] = u'%s\r\n%s%s\r\n' \
1164                %(head, mailto, body_text)
1165   
1166        ## Save the attachments to the ticket   
1167        #
1168        error_with_attachments =  self.attach_attachments(message_parts)
1169
1170        if error_with_attachments:
1171            changed = True
1172            comment = '%s\n%s\n' %(comment, error_with_attachments)
1173
1174        if changed:
1175            if self.parameters.dry_run:
1176                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1177            else:
1178                tkt.save_changes(tkt['reporter'], comment)
1179                #print tkt.get_changelog(self.db, when)
1180
1181        if not spam:
1182            self.notify(tkt, True)
1183
1184
1185    def attach_attachments(self, message_parts, update=False):
1186        '''
1187        save any attachments as files in the ticket's directory
1188        '''
1189        self.logger.debug('function attach_attachments()')
1190
1191        if self.parameters.dry_run:
1192            print "DRY_RUN: no attachments attached to tickets"
1193            return ''
1194
1195        count = 0
1196
1197        ## Get Maxium attachment size
1198        #
1199        max_size = int(self.get_config('attachment', 'max_size'))
1200        status   = None
1201       
1202        for item in message_parts:
1203            ## Skip body parts
1204            #
1205            if not isinstance(item, tuple):
1206                continue
1207               
1208            (original, filename, part) = item
1209
1210            ## We have to determine the size so we use this temporary solution. we must escape it
1211            #  else we get UnicodeErrors.
1212            #
1213            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename)))
1214            text = part.get_payload(decode=1)
1215            if not text:
1216                text = '(None)'
1217            fd.write(text)
1218            fd.close()
1219
1220            ## get the file_size
1221            #
1222            stats = os.lstat(path)
1223            file_size = stats[ST_SIZE]
1224
1225            ## Check if the attachment size is allowed
1226            #
1227            if (max_size != -1) and (file_size > max_size):
1228                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1229                    %(status, original, file_size, max_size)
1230
1231                os.unlink(path)
1232                continue
1233            else:
1234                count = count + 1
1235                   
1236            ## Insert the attachment
1237            #
1238            fd = open(path, 'rb')
1239            if self.system == 'discussion':
1240                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1241                  % (self.id,))
1242            else:
1243                self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id))
1244                att = attachment.Attachment(self.env, 'ticket', self.id)
1245 
1246            ## This will break the ticket_update system, the body_text is vaporized
1247            #  ;-(
1248            #
1249            if not update:
1250                att.author = self.author
1251                att.description = self.email_to_unicode('Added by email2trac')
1252
1253            try:
1254                self.logger.debug('Insert atachment')
1255                att.insert(filename, fd, file_size)
1256            except OSError, detail:
1257                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1258                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
1259
1260            ## Remove the created temporary filename
1261            #
1262            fd.close()
1263            os.unlink(path)
1264
1265        ## return error
1266        #
1267        return status
1268
1269########## Fullblog functions  #################################################
1270
1271    def blog(self, id):
1272        """
1273        The blog create/update function
1274        """
1275        ## import the modules
1276        #
1277        from tracfullblog.core import FullBlogCore
1278        from tracfullblog.model import BlogPost, BlogComment
1279        from trac.test import Mock, MockPerm
1280
1281        ## instantiate blog core
1282        #
1283        blog = FullBlogCore(self.env)
1284        req = Mock(authname='anonymous', perm=MockPerm(), args={})
1285
1286        if id:
1287
1288            ## update blog
1289            #
1290            comment = BlogComment(self.env, id)
1291            comment.author = self.author
1292
1293            message_parts = self.get_message_parts(m)
1294            comment.comment = self.get_body_text(message_parts)
1295
1296            blog.create_comment(req, comment)
1297
1298        else:
1299            ## create blog
1300            #
1301            import time
1302            post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1303
1304            #post = BlogPost(self.env, blog._get_default_postname(self.env))
1305           
1306            post.author = self.author
1307            post.title = self.email_to_unicode(m['Subject'])
1308
1309            message_parts = self.get_message_parts(m)
1310            post.body = self.get_body_text(message_parts)
1311           
1312            blog.create_post(req, post, self.author, u'Created by email2trac', False)
1313
1314
1315########## Discussion functions  ##############################################
1316
1317    def discussion_topic(self, content, subject):
1318
1319        ## Import modules.
1320        #
1321        from tracdiscussion.api import DiscussionApi
1322        from trac.util.datefmt import to_timestamp, utc
1323
1324        self.logger.debug('Creating a new topic in forum:', self.id)
1325
1326        ## Get dissussion API component.
1327        #
1328        api = self.env[DiscussionApi]
1329        context = self._create_context(content, subject)
1330
1331        ## Get forum for new topic.
1332        #
1333        forum = api.get_forum(context, self.id)
1334
1335        if not forum:
1336            self.logger.error("ERROR: Replied forum doesn't exist")
1337
1338        ## Prepare topic.
1339        #
1340        topic = {'forum' : forum['id'],
1341                 'subject' : context.subject,
1342                 'time': to_timestamp(datetime.now(utc)),
1343                 'author' : self.author,
1344                 'subscribers' : [self.email_addr],
1345                 'body' : self.get_body_text(context.content_parts)}
1346
1347        ## Add topic to DB and commit it.
1348        #
1349        self._add_topic(api, context, topic)
1350        self.db.commit()
1351
1352    def discussion_topic_reply(self, content, subject):
1353
1354        ## Import modules.
1355        #
1356        from tracdiscussion.api import DiscussionApi
1357        from trac.util.datefmt import to_timestamp, utc
1358
1359        self.logger.debug('Replying to discussion topic', self.id)
1360
1361        ## Get dissussion API component.
1362        #
1363        api = self.env[DiscussionApi]
1364        context = self._create_context(content, subject)
1365
1366        ## Get replied topic.
1367        #
1368        topic = api.get_topic(context, self.id)
1369
1370        if not topic:
1371            self.logger.error("ERROR: Replied topic doesn't exist")
1372
1373        ## Prepare message.
1374        #
1375        message = {'forum' : topic['forum'],
1376                   'topic' : topic['id'],
1377                   'replyto' : -1,
1378                   'time' : to_timestamp(datetime.now(utc)),
1379                   'author' : self.author,
1380                   'body' : self.get_body_text(context.content_parts)}
1381
1382        ## Add message to DB and commit it.
1383        #
1384        self._add_message(api, context, message)
1385        self.db.commit()
1386
1387    def discussion_message_reply(self, content, subject):
1388
1389        ## Import modules.
1390        #
1391        from tracdiscussion.api import DiscussionApi
1392        from trac.util.datefmt import to_timestamp, utc
1393
1394        self.logger.debug('Replying to discussion message', self.id)
1395
1396        ## Get dissussion API component.
1397        #
1398        api = self.env[DiscussionApi]
1399        context = self._create_context(content, subject)
1400
1401        ## Get replied message.
1402        #
1403        message = api.get_message(context, self.id)
1404
1405        if not message:
1406            self.logger.error("ERROR: Replied message doesn't exist")
1407
1408        ## Prepare message.
1409        #
1410        message = {'forum' : message['forum'],
1411                   'topic' : message['topic'],
1412                   'replyto' : message['id'],
1413                   'time' : to_timestamp(datetime.now(utc)),
1414                   'author' : self.author,
1415                   'body' : self.get_body_text(context.content_parts)}
1416
1417        ## Add message to DB and commit it.
1418        #
1419        self._add_message(api, context, message)
1420        self.db.commit()
1421
1422    def _create_context(self, content, subject):
1423
1424        ## Import modules.
1425        #
1426        from trac.mimeview import Context
1427        from trac.web.api import Request
1428        from trac.perm import PermissionCache
1429
1430        ## TODO: Read server base URL from config.
1431        #  Create request object to mockup context creation.
1432        #
1433        environ = {'SERVER_PORT' : 80,
1434                   'SERVER_NAME' : 'test',
1435                   'REQUEST_METHOD' : 'POST',
1436                   'wsgi.url_scheme' : 'http',
1437                   'wsgi.input' : sys.stdin}
1438        chrome =  {'links': {},
1439                   'scripts': [],
1440                   'ctxtnav': [],
1441                   'warnings': [],
1442                   'notices': []}
1443
1444        if self.env.base_url_for_redirect:
1445            environ['trac.base_url'] = self.env.base_url
1446
1447        req = Request(environ, None)
1448        req.chrome = chrome
1449        req.tz = 'missing'
1450        req.authname = self.author
1451        req.perm = PermissionCache(self.env, self.author)
1452        req.locale = None
1453
1454        ## Create and return context.
1455        #
1456        context = Context.from_request(req)
1457        context.realm = 'discussion-email2trac'
1458        context.cursor = self.db.cursor()
1459        context.content = content
1460        context.subject = subject
1461
1462        ## Read content parts from content.
1463        #
1464        context.content_parts = self.get_message_parts(content)
1465        context.content_parts = self.unique_attachment_names(
1466          context.content_parts)
1467
1468        return context
1469
1470    def _add_topic(self, api, context, topic):
1471        context.req.perm.assert_permission('DISCUSSION_APPEND')
1472
1473        ## Filter topic.
1474        #
1475        for discussion_filter in api.discussion_filters:
1476            accept, topic_or_error = discussion_filter.filter_topic(
1477              context, topic)
1478            if accept:
1479                topic = topic_or_error
1480            else:
1481                raise TracError(topic_or_error)
1482
1483        ## Add a new topic.
1484        #
1485        api.add_topic(context, topic)
1486
1487        ## Get inserted topic with new ID.
1488        #
1489        topic = api.get_topic_by_time(context, topic['time'])
1490
1491        ## Attach attachments.
1492        #
1493        self.id = topic['id']
1494        self.attach_attachments(context.content_parts, True)
1495
1496        ## Notify change listeners.
1497        #
1498        for listener in api.topic_change_listeners:
1499            listener.topic_created(context, topic)
1500
1501    def _add_message(self, api, context, message):
1502        context.req.perm.assert_permission('DISCUSSION_APPEND')
1503
1504        ## Filter message.
1505        #
1506        for discussion_filter in api.discussion_filters:
1507            accept, message_or_error = discussion_filter.filter_message(
1508              context, message)
1509            if accept:
1510                message = message_or_error
1511            else:
1512                raise TracError(message_or_error)
1513
1514        ## Add message.
1515        #
1516        api.add_message(context, message)
1517
1518        ## Get inserted message with new ID.
1519        #
1520        message = api.get_message_by_time(context, message['time'])
1521
1522        ## Attach attachments.
1523        #
1524        self.id = message['topic']
1525        self.attach_attachments(context.content_parts, True)
1526
1527        ## Notify change listeners.
1528        #
1529        for listener in api.message_change_listeners:
1530            listener.message_created(context, message)
1531
1532########## MAIN function  ######################################################
1533
1534    def parse(self, fp):
1535        """
1536        """
1537        self.logger.debug('Main function parse')
1538        global m
1539
1540        m = email.message_from_file(fp)
1541       
1542        if not m:
1543            self.logger.debug('This is not a valid email message format')
1544            return
1545           
1546        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1547        #
1548        try:
1549            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1550        except AttributeError, detail:
1551            pass
1552
1553        if self.parameters.debug:     # save the entire e-mail message text
1554            self.save_email_for_debug(m, self.parameters.project_name, True)
1555
1556        self.db = self.env.get_db_cnx()
1557        self.get_sender_info(m)
1558
1559        if not self.email_header_acl('white_list', self.email_addr, True):
1560            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1561            return False
1562
1563        if self.email_header_acl('black_list', self.email_addr, False):
1564            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1565            return False
1566
1567        if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1568            self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr))
1569            return False
1570
1571        ## If spam drop the message
1572        #
1573        if self.spam(m) == 'drop':
1574            return False
1575
1576        elif self.spam(m) == 'spam':
1577            spam_msg = True
1578        else:
1579            spam_msg = False
1580
1581        if not m['Subject']:
1582            subject  = 'No Subject'
1583        else:
1584            subject  = self.email_to_unicode(m['Subject'])
1585
1586        self.logger.info('subject: %s' %subject)
1587
1588        ## [hic] #1529: Re: LRZ
1589        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1590        #
1591        ticket_regex = r'''
1592            (?P<new_fields>[#][?].*)
1593            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1594            '''
1595        ## Check if  FullBlogPlugin is installed
1596        #
1597        blog_enabled = None
1598        blog_regex = ''
1599        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1600            self.logger.debug('Trac BLOG support enabled')
1601            blog_enabled = True
1602            blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
1603
1604
1605        ## Check if DiscussionPlugin is installed
1606        #
1607        discussion_enabled = None
1608        discussion_regex = ''
1609        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1610            self.logger.debug('Trac Discussion support enabled')
1611            discussion_enabled = True
1612            discussion_regex = r'''
1613            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1614            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1615            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1616            '''
1617
1618
1619        regex_str = ticket_regex + blog_regex + discussion_regex
1620        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1621
1622        ## Find out if this is a ticket, a blog or a discussion
1623        #
1624        result =  SYSTEM_RE.search(subject)
1625
1626        if result:
1627            ## update ticket + fields
1628            #
1629            if result.group('reply'):
1630                self.system = 'ticket'
1631
1632                ## Skip the last ':' character
1633                #
1634                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1635                    self.new_ticket(m, subject, spam_msg)
1636
1637            ## New ticket + fields
1638            #
1639            elif result.group('new_fields'):
1640                self.system = 'ticket'
1641                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1642
1643            if blog_enabled:
1644                if result.group('blog'):
1645                    self.system = 'blog'
1646                    self.blog(result.group('blog_id'))
1647
1648            if discussion_enabled:
1649                ## New topic.
1650                #
1651                if result.group('forum'):
1652                    self.system = 'discussion'
1653                    self.id = int(result.group('forum_id'))
1654                    self.discussion_topic(m, subject[result.end('forum'):])
1655
1656                ## Reply to topic.
1657                #
1658                elif result.group('topic'):
1659                    self.system = 'discussion'
1660                    self.id = int(result.group('topic_id'))
1661                    self.discussion_topic_reply(m, subject[result.end('topic'):])
1662
1663                ## Reply to topic message.
1664                #
1665                elif result.group('message'):
1666                    self.system = 'discussion'
1667                    self.id = int(result.group('message_id'))
1668                    self.discussion_message_reply(m, subject[result.end('message'):])
1669
1670        else:
1671            self.system = 'ticket'
1672            (matched_id, subject) = self.ticket_update_by_subject(subject)
1673            if matched_id:
1674                if not self.ticket_update(m, matched_id, spam_msg):
1675                    self.new_ticket(m, subject, spam_msg)
1676            else:
1677                ## No update by subject, so just create a new ticket
1678                #
1679                self.new_ticket(m, subject, spam_msg)
1680
1681
1682########## BODY TEXT functions  ###########################################################
1683
1684    def strip_signature(self, text):
1685        """
1686        Strip signature from message, inspired by Mailman software
1687        """
1688        self.logger.debug('function strip_signature')
1689
1690        body = []
1691        for line in text.splitlines():
1692            if line == '-- ':
1693                break
1694            body.append(line)
1695
1696        return ('\n'.join(body))
1697
1698    def reflow(self, text, delsp = 0):
1699        """
1700        Reflow the message based on the format="flowed" specification (RFC 3676)
1701        """
1702        flowedlines = []
1703        quotelevel = 0
1704        prevflowed = 0
1705
1706        for line in text.splitlines():
1707            from re import match
1708           
1709            ## Figure out the quote level and the content of the current line
1710            #
1711            m = match('(>*)( ?)(.*)', line)
1712            linequotelevel = len(m.group(1))
1713            line = m.group(3)
1714
1715            ## Determine whether this line is flowed
1716            #
1717            if line and line != '-- ' and line[-1] == ' ':
1718                flowed = 1
1719            else:
1720                flowed = 0
1721
1722            if flowed and delsp and line and line[-1] == ' ':
1723                line = line[:-1]
1724
1725            ## If the previous line is flowed, append this line to it
1726            #
1727            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1728                flowedlines[-1] += line
1729
1730            ## Otherwise, start a new line
1731            #
1732            else:
1733                flowedlines.append('>' * linequotelevel + line)
1734
1735            prevflowed = flowed
1736           
1737
1738        return '\n'.join(flowedlines)
1739
1740    def strip_quotes(self, text):
1741        """
1742        Strip quotes from message by Nicolas Mendoza
1743        """
1744        self.logger.debug('function strip_quotes')
1745
1746        body = []
1747        for line in text.splitlines():
1748            try:
1749
1750                if line.startswith(self.parameters.email_quote):
1751                    continue
1752
1753            except UnicodeDecodeError:
1754
1755                tmp_line = self.email_to_unicode(line)
1756                if tmp_line.startswith(self.parameters.email_quote):
1757                    continue
1758               
1759            body.append(line)
1760
1761        return ('\n'.join(body))
1762
1763    def inline_properties(self, text):
1764        """
1765        Parse text if we use inline keywords to set ticket fields
1766        """
1767        self.logger.debug('function inline_properties')
1768
1769        properties = dict()
1770        body = list()
1771
1772        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
1773
1774        for line in text.splitlines():
1775            match = INLINE_EXP.match(line)
1776            if match:
1777                keyword, value = match.groups()
1778
1779                if self.parameters.inline_properties_first_wins:
1780                    if keyword in self.properties.keys():
1781                        continue
1782
1783                self.properties[keyword] = value.strip()
1784                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
1785
1786            else:
1787                body.append(line)
1788               
1789        return '\n'.join(body)
1790
1791
1792    def wrap_text(self, text, replace_whitespace = False):
1793        """
1794        Will break a lines longer then given length into several small
1795        lines of size given length
1796        """
1797        import textwrap
1798
1799        LINESEPARATOR = '\n'
1800        reformat = ''
1801
1802        for s in text.split(LINESEPARATOR):
1803            tmp = textwrap.fill(s, self.parameters.use_textwrap)
1804            if tmp:
1805                reformat = '%s\n%s' %(reformat,tmp)
1806            else:
1807                reformat = '%s\n' %reformat
1808
1809        return reformat
1810
1811        # Python2.4 and higher
1812        #
1813        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1814        #
1815
1816########## EMAIL attachements functions ###########################################################
1817
1818    def inline_part(self, part):
1819        """
1820        """
1821        self.logger.debug('function inline_part()')
1822
1823        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1824
1825    def get_message_parts(self, msg):
1826        """
1827        parses the email message and returns a list of body parts and attachments
1828        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1829        """
1830        self.logger.debug('function get_message_parts()')
1831
1832        message_parts = list()
1833   
1834        ALTERNATIVE_MULTIPART = False
1835
1836        for part in msg.walk():
1837            content_maintype = part.get_content_maintype()
1838            content_type =  part.get_content_type()
1839
1840            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
1841            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
1842
1843            ## Check content type
1844            #
1845            if content_type in self.STRIP_CONTENT_TYPES:
1846                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
1847                continue
1848
1849            ## Catch some mulitpart execptions
1850            #
1851            if content_type == 'multipart/alternative':
1852                ALTERNATIVE_MULTIPART = True
1853                continue
1854
1855            ## Skip multipart containers
1856            #
1857            if content_maintype == 'multipart':
1858                self.logger.debug("\t Skipping multipart container")
1859                continue
1860           
1861            ## 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"
1862            #
1863            inline = self.inline_part(part)
1864
1865            ## Drop HTML message
1866            #
1867            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
1868                if content_type == 'text/html':
1869                    self.logger.debug('\t Skipping alternative HTML message')
1870                    ALTERNATIVE_MULTIPART = False
1871                    continue
1872
1873
1874            ## Save all non plain text message as attachment
1875            #
1876            if not content_type in ['text/plain']:
1877
1878                if self.parameters.debug:
1879                    s = '\t Filename: %s' % part.get_filename()
1880                    self.print_unicode(s)
1881
1882                ## First try to use email header function to convert filename.
1883                #  If this fails the use the plain filename
1884                #
1885                try:
1886                    filename = self.email_to_unicode(part.get_filename())
1887                except UnicodeEncodeError, detail:
1888                    filename = part.get_filename()
1889
1890                message_parts.append((filename, part))
1891
1892                ## We van only convert html messages
1893                #
1894                if not content_type == 'text/html':
1895                    self.logger.debug('\t Appending content_type = %s' %(content_type))
1896                    continue
1897
1898            if not inline:
1899                    self.logger.debug('\t Skipping %s, not an inline messsage part' %(content_type))
1900                    continue
1901               
1902            ## Try to decode message part. We have a html or plain text messafe
1903            #
1904            body_text = part.get_payload(decode=1)
1905            if not body_text:           
1906                body_text = part.get_payload(decode=0)
1907
1908            ## Try to convert html message
1909            #
1910            if content_type == 'text/html':
1911
1912                body_text = self.html_2_txt(body_text)
1913                if not body_text:
1914                    continue
1915
1916            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1917            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1918
1919            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
1920                body_text = self.reflow(body_text, delsp == 'yes')
1921   
1922            if self.parameters.strip_signature:
1923                body_text = self.strip_signature(body_text)
1924
1925            if self.parameters.strip_quotes:
1926                body_text = self.strip_quotes(body_text)
1927
1928            if self.parameters.inline_properties:
1929                body_text = self.inline_properties(body_text)
1930
1931            if self.parameters.use_textwrap:
1932                body_text = self.wrap_text(body_text)
1933
1934            ## Get contents charset (iso-8859-15 if not defined in mail headers)
1935            #
1936            charset = part.get_content_charset()
1937            if not charset:
1938                charset = 'iso-8859-15'
1939
1940            try:
1941                ubody_text = unicode(body_text, charset)
1942
1943            except UnicodeError, detail:
1944                ubody_text = unicode(body_text, 'iso-8859-15')
1945
1946            except LookupError, detail:
1947                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1948
1949            if self.parameters.verbatim_format:
1950                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1951            else:
1952                message_parts.append('%s' %ubody_text)
1953
1954        return message_parts
1955       
1956    def unique_attachment_names(self, message_parts):
1957        """
1958        Make sure we have unique names attachments:
1959          - check if it contains illegal characters
1960          - Rename "None" filenames to "untitled-part"
1961        """
1962        self.logger.debug('function unique_attachment_names()')
1963        renamed_parts = []
1964        attachment_names = set()
1965
1966        for item in message_parts:
1967           
1968            ## If not an attachment, leave it alone
1969            #
1970            if not isinstance(item, tuple):
1971                renamed_parts.append(item)
1972                continue
1973               
1974            (filename, part) = item
1975
1976            ## If filename = None, use a default one
1977            #
1978            if filename in [ 'None']:
1979                filename = 'untitled-part'
1980                self.logger.info('Rename filename "None" to: %s' %filename)
1981
1982                ## Guess the extension from the content type, use non strict mode
1983                #  some additional non-standard but commonly used MIME types
1984                #  are also recognized
1985                #
1986                ext = mimetypes.guess_extension(part.get_content_type(), False)
1987                if not ext:
1988                    ext = '.bin'
1989
1990                filename = '%s%s' % (filename, ext)
1991
1992            ## Discard relative paths for windows/unix in attachment names
1993            #
1994            #filename = filename.replace('\\', '/').replace(':', '/')
1995            filename = filename.replace('\\', '_')
1996            filename = filename.replace('/', '_')
1997
1998            ## remove linefeed char
1999            #
2000            for forbidden_char in ['\r', '\n']:
2001                filename = filename.replace(forbidden_char,'')
2002
2003            ## We try to normalize the filename to utf-8 NFC if we can.
2004            #  Files uploaded from OS X might be in NFD.
2005            #  Check python version and then try it
2006            #
2007            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2008            #   try:
2009            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2010            #   except TypeError:
2011            #       pass
2012
2013            ## Make the filename unique for this ticket
2014            #
2015            num = 0
2016            unique_filename = filename
2017            dummy_filename, ext = os.path.splitext(filename)
2018
2019            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2020                num += 1
2021                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2022               
2023            if self.parameters.debug:
2024                s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2025                self.print_unicode(s)
2026
2027            attachment_names.add(unique_filename)
2028
2029            renamed_parts.append((filename, unique_filename, part))
2030   
2031        return renamed_parts
2032           
2033           
2034    def attachment_exists(self, filename):
2035
2036        if self.parameters.debug:
2037            s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2038            self.print_unicode(s)
2039
2040        ## We have no valid ticket id
2041        #
2042        if not self.id:
2043            return False
2044
2045        try:
2046            if self.system == 'discussion':
2047                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
2048                  % (self.id,), filename)
2049            else:
2050                att = attachment.Attachment(self.env, 'ticket', self.id,
2051                  filename)
2052            return True
2053        except attachment.ResourceNotFound:
2054            return False
2055
2056########## TRAC Ticket Text ###########################################################
2057           
2058    def get_body_text(self, message_parts):
2059        """
2060        """
2061        self.logger.debug('function get_body_text()')
2062
2063        body_text = []
2064       
2065        for part in message_parts:
2066       
2067            ## Plain text part, append it
2068            #
2069            if not isinstance(part, tuple):
2070                body_text.extend(part.strip().splitlines())
2071                body_text.append("")
2072                continue
2073
2074            (original, filename, part) = part
2075            inline = self.inline_part(part)
2076
2077            ## Skip generation of attachment link if html is converted to text
2078            #
2079            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2080                s = 'Skipping attachment link for html part: %s' %(filename)
2081                self.print_unicode(s)
2082                continue
2083           
2084            if part.get_content_maintype() == 'image' and inline:
2085                if self.system != 'discussion':
2086                    s = 'wiki image link for: %s' %(filename)
2087                    self.print_unicode(s)
2088                    body_text.append('[[Image(%s)]]' % filename)
2089                body_text.append("")
2090            else:
2091                if self.system != 'discussion':
2092                    s = 'wiki attachment link for: %s' %(filename)
2093                    self.print_unicode(s)
2094                    body_text.append('[attachment:"%s"]' % filename)
2095                body_text.append("")
2096
2097        ## Convert list body_texts to string
2098        #
2099        body_text = '\r\n'.join(body_text)
2100        return body_text
2101
2102    def html_mailto_link(self, subject):
2103        """
2104        This function returns a HTML mailto tag with the ticket id and author email address
2105        """
2106        self.logger.debug("function html_mailto_link")
2107        if not self.author:
2108            author = self.email_addr
2109        else:   
2110            author = self.author
2111
2112        if not self.parameters.mailto_cc:
2113            self.parameters.mailto_cc = ''
2114
2115        ## use urllib to escape the chars
2116        #
2117        s = '%s?Subject=%s&cc=%s' %(
2118               urllib.quote(self.email_addr),
2119               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2120               urllib.quote(self.parameters.mailto_cc)
2121               )
2122
2123        if self.VERSION in [ 0.10 ]:
2124            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)
2125        else:
2126            s = '[mailto:"%s" Reply to: %s]' %(s, author)
2127
2128        self.logger.debug("\tmailto link %s" %s)
2129        return s
2130
2131########## TRAC notify section ###########################################################
2132
2133    def notify(self, tkt, new=True, modtime=0):
2134        """
2135        A wrapper for the TRAC notify function. So we can use templates
2136        """
2137        self.logger.debug('function notify()')
2138
2139        if self.parameters.notify_reporter:
2140            self.logger.debug('\t Notify reporter set')
2141            global sender_email
2142            sender_email = self.email_addr
2143 
2144            self.logger.debug('\t Using Email2TracNotification function AlwaysNotifyReporter')
2145            import trac.notification as Email2TracNotification
2146            Email2TracNotification.Notify.notify = AlwaysNotifyReporter
2147
2148        if self.parameters.dry_run  :
2149                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
2150                return
2151        try:
2152
2153            #from trac.ticket.web_ui import TicketModule
2154            #from trac.ticket.notification import TicketNotificationSystem
2155            #ticket_sys = TicketNotificationSystem(self.env)
2156            #a = TicketModule(self.env)
2157            #print a.__dict__
2158            #tn_sys = TicketNotificationSystem(self.env)
2159            #print tn_sys
2160            #print tn_sys.__dict__
2161            #sys.exit(0)
2162
2163            ## create false {abs_}href properties, to trick Notify()
2164            #
2165            if not (self.VERSION in [0.11, 0.12]):
2166                self.env.abs_href = Href(self.get_config('project', 'url'))
2167                self.env.href = Href(self.get_config('project', 'url'))
2168
2169            tn = TicketNotifyEmail(self.env)
2170
2171            if self.parameters.alternate_notify_template:
2172
2173                if self.VERSION >= 0.11:
2174
2175                    from trac.web.chrome import Chrome
2176
2177                    if  self.parameters.alternate_notify_template_update and not new:
2178                        tn.template_name = self.parameters.alternate_notify_template_update
2179                    else:
2180                        tn.template_name = self.parameters.alternate_notify_template
2181
2182                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2183                       
2184                else:
2185
2186                    tn.template_name = self.parameters.alternate_notify_template
2187
2188            tn.notify(tkt, new, modtime)
2189
2190        except Exception, e:
2191            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
2192
2193########## END Class Definition  ########################################################
2194
2195########## Global Notificaition Function ################################################
2196def AlwaysNotifyReporter(self, resid):
2197    """
2198    Copy of def notify() to manipulate recipents to always include reporter for the
2199    notification.
2200    """
2201    (torcpts, ccrcpts) = self.get_recipients(resid)
2202
2203    if not tktparser.email_header_acl('notify_reporter_black_list', sender_email, False):
2204        ## additionally append sender (regardeless of settings in trac.ini)
2205        #
2206        if not sender_email in torcpts:
2207            torcpts.append(sender_email)
2208
2209    self.begin_send()
2210    self.send(torcpts, ccrcpts)
2211    self.finish_send()
2212
2213########## Parse Config File  ###########################################################
2214
2215def ReadConfig(file, name):
2216    """
2217    Parse the config file
2218    """
2219    if not os.path.isfile(file):
2220        print 'File %s does not exist' %file
2221        sys.exit(1)
2222
2223    config = trac_config.Configuration(file)
2224   
2225    parentdir = config.get('DEFAULT', 'parentdir')
2226    sections = config.sections()
2227
2228    ## use some trac internals to get the defaults
2229    #
2230    tmp = config.parser.defaults()
2231    project =  SaraDict()
2232
2233    for option, value in tmp.items():
2234        try:
2235            project[option] = int(value)
2236        except ValueError:
2237            project[option] = value
2238
2239    if name:
2240        if name in sections:
2241            project =  SaraDict()
2242            for option, value in  config.options(name):
2243                try:
2244                    project[option] = int(value)
2245                except ValueError:
2246                    project[option] = value
2247
2248        elif not parentdir:
2249            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2250            print "or set parentdir in the [DEFAULT] section"
2251            sys.exit(1)
2252
2253    ## If parentdir then set project dir to parentdir + name
2254    #
2255    if not project.has_key('project'):
2256        if not parentdir:
2257            print "You must set project or parentdir in your configuration file"
2258            sys.exit(1)
2259        elif not name:
2260            print "You must configure a  project section in your configuration file"
2261        else:
2262            project['project'] = os.path.join(parentdir, name)
2263
2264    ##
2265    # Save the project name
2266    #
2267    project['project_name'] = os.path.basename(project['project'])
2268
2269    return project
2270
2271########## Setup Logging ###############################################################
2272
2273def setup_log(parameters, project_name, interactive=None):
2274    """
2275    Setup logging
2276
2277    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
2278    would be interpreted by the ConfigParser itself.
2279    """
2280    logger = logging.getLogger('email2trac %s' %project_name)
2281
2282    if interactive:
2283        parameters.log_type = 'stderr'
2284
2285    if not parameters.log_type:
2286        if sys.platform in ['win32', 'cygwin']:
2287            parameters.log_type = 'eventlog'
2288        else:
2289            parameters.log_type = 'syslog'
2290
2291    if parameters.log_type == 'file':
2292
2293        if not parameters.log_file:
2294            parameters.log_file = 'email2trac.log'
2295
2296        if not os.path.isabs(parameters.log_file):
2297            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
2298
2299        log_handler = logging.FileHandler(parameters.log_file)
2300
2301    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2302        ## Requires win32 extensions
2303        #
2304        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
2305
2306    elif parameters.log_type in ('syslog', 'unix'):
2307        log_handler = logging.handlers.SysLogHandler('/dev/log')
2308
2309    elif parameters.log_type in ('stderr'):
2310        log_handler = logging.StreamHandler(sys.stderr)
2311
2312    else:
2313        log_handler = logging.handlers.BufferingHandler(0)
2314
2315    if parameters.log_format:
2316        parameters.log_format = parameters.log_format.replace('$(', '%(')
2317    else:
2318        parameters.log_format = '%(name)s: %(message)s'
2319        if parameters.log_type in ('file', 'stderr'):
2320            parameters.log_format = '%(asctime)s ' + parameters.log_format
2321
2322    log_formatter = logging.Formatter(parameters.log_format)
2323    log_handler.setFormatter(log_formatter)
2324    logger.addHandler(log_handler)
2325
2326    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2327        logger.setLevel(logging.DEBUG)
2328        parameters.debug = 1
2329
2330    elif parameters.log_level in ['INFO'] or parameters.verbose:
2331        logger.setLevel(logging.INFO)
2332
2333    elif parameters.log_level in ['WARNING']:
2334        logger.setLevel(logging.WARNING)
2335
2336    elif parameters.log_level in ['ERROR']:
2337        logger.setLevel(logging.ERROR)
2338
2339    elif parameters.log_level in ['CRITICAL']:
2340        logger.setLevel(logging.CRITICAL)
2341
2342    else:
2343        logger.setLevel(logging.INFO)
2344
2345    return logger
2346
2347
2348if __name__ == '__main__':
2349    ## Default config file
2350    #
2351    configfile = '@email2trac_conf@'
2352    project = ''
2353    component = ''
2354    ticket_prefix = 'default'
2355    dry_run = None
2356    verbose = None
2357    debug_interactive = None
2358
2359    SHORT_OPT = 'cdhf:np:t:v'
2360    LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
2361
2362    try:
2363        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2364    except getopt.error,detail:
2365        print __doc__
2366        print detail
2367        sys.exit(1)
2368   
2369    project_name = None
2370    for opt,value in opts:
2371        if opt in [ '-h', '--help']:
2372            print __doc__
2373            sys.exit(0)
2374        elif opt in ['-c', '--component']:
2375            component = value
2376        elif opt in ['-d', '--debug']:
2377            debug_interactive = 1
2378        elif opt in ['-f', '--file']:
2379            configfile = value
2380        elif opt in ['-n', '--dry-run']:
2381            dry_run = True
2382        elif opt in ['-p', '--project']:
2383            project_name = value
2384        elif opt in ['-t', '--ticket_prefix']:
2385            ticket_prefix = value
2386        elif opt in ['-v', '--verbose']:
2387            verbose = True
2388   
2389    settings = ReadConfig(configfile, project_name)
2390
2391    ## The default prefix for ticket values in email2trac.conf
2392    #
2393    settings.ticket_prefix = ticket_prefix
2394    settings.dry_run = dry_run
2395    settings.verbose = verbose
2396
2397    if not settings.debug and debug_interactive:
2398        settings.debug = debug_interactive
2399
2400    if not settings.project:
2401        print __doc__
2402        print 'No Trac project is defined in the email2trac config file.'
2403        sys.exit(1)
2404
2405    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2406   
2407    if component:
2408        settings['component'] = component
2409
2410    ## Determine major trac version used to be in email2trac.conf
2411    # Quick hack for 0.12
2412    #
2413    version = '0.%s' %(trac_version.split('.')[1])
2414    if version.startswith('0.12'):
2415        version = '0.12'
2416
2417    logger.debug("Found trac version: %s" %(version))
2418   
2419    try:
2420        if version == '0.10':
2421            from trac import attachment
2422            from trac.env import Environment
2423            from trac.ticket import Ticket
2424            from trac.web.href import Href
2425            from trac import util
2426            from trac.ticket.web_ui import TicketModule
2427
2428            #
2429            # return  util.text.to_unicode(str)
2430            #
2431            # see http://projects.edgewall.com/trac/changeset/2799
2432            from trac.ticket.notification import TicketNotifyEmail
2433            from trac import config as trac_config
2434            from trac.core import TracError
2435
2436        elif version == '0.11':
2437            from trac import attachment
2438            from trac.env import Environment
2439            from trac.ticket import Ticket
2440            from trac.web.href import Href
2441            from trac import config as trac_config
2442            from trac import util
2443            from trac.core import TracError
2444            from trac.perm import PermissionSystem
2445            from trac.ticket.web_ui import TicketModule
2446
2447            #
2448            # return  util.text.to_unicode(str)
2449            #
2450            # see http://projects.edgewall.com/trac/changeset/2799
2451            from trac.ticket.notification import TicketNotifyEmail
2452
2453        elif version == '0.12':
2454            from trac import attachment
2455            from trac.env import Environment
2456            from trac.ticket import Ticket
2457            from trac.web.href import Href
2458            from trac import config as trac_config
2459            from trac import util
2460            from trac.core import TracError
2461            from trac.perm import PermissionSystem
2462            from trac.ticket.web_ui import TicketModule
2463
2464            #
2465            # return  util.text.to_unicode(str)
2466            #
2467            # see http://projects.edgewall.com/trac/changeset/2799
2468            from trac.ticket.notification import TicketNotifyEmail
2469
2470
2471        else:
2472            logger.error('TRAC version %s is not supported' %version)
2473            sys.exit(0)
2474
2475        ## Must be set before environment is created
2476        #
2477        if settings.has_key('python_egg_cache'):
2478            python_egg_cache = str(settings['python_egg_cache'])
2479            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2480
2481        if settings.debug > 0:
2482            logger.debug('Loading environment %s', settings.project)
2483
2484        try:
2485            env = Environment(settings['project'], create=0)
2486        except IOError, detail:
2487            logger.error("Trac project does not exists: %s" %settings['project'])
2488            sys.exit(0)
2489
2490        tktparser = TicketEmailParser(env, settings, logger, float(version))
2491        tktparser.parse(sys.stdin)
2492
2493    ## Catch all errors and use the logging module
2494    #
2495    except Exception, error:
2496
2497        etype, evalue, etb = sys.exc_info()
2498        for e in traceback.format_exception(etype, evalue, etb):
2499            logger.critical(e)
2500
2501        if m:
2502            tktparser.save_email_for_debug(m, settings.project_name, True)
2503
2504        sys.exit(1)
2505# EOB
Note: See TracBrowser for help on using the repository browser.