source: trunk/email2trac.py.in @ 556

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

chamged workflow to TicketModule? implementation as suggested by: Holger Jürgs, see #226

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