source: trunk/email2trac.py.in @ 520

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

fixed a bug in the date format used for ticket_update_by_subject.

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