source: trunk/email2trac.py.in @ 550

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

fixed an error in check_filename_length must return 'None' if filename is empty. see #247

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