source: trunk/email2trac.py.in @ 561

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

converted all dry-run print statement to self.logger.info

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