source: trunk/email2trac.py.in @ 527

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

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