source: trunk/email2trac.py.in @ 528

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

email2trac.py.in:

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