source: trunk/email2trac.py.in @ 548

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

email2trac can now handle large filenames, trac report an error, see #247

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