source: trunk/email2trac.py.in @ 570

Last change on this file since 570 was 570, checked in by bas, 10 years ago

use all 'to-addresses for recipient_list instead of just one, closes #268

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