source: trunk/email2trac.py.in @ 554

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

version 0.11, 0.12 and 0.13 have the same import statements.

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