source: trunk/email2trac.py.in @ 506

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

fixed mailto link buf for trac 0.12

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