source: trunk/email2trac.py.in @ 526

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

Fix re-opening for closed tickets for trac 0.12 and higher. Only triggered when no workflow was defined in email2trac.conf, see #247

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