source: trunk/email2trac.py.in @ 555

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

rewrote save_email_for_debug. So no unicode problems and all files have the same prefix, see #247

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