source: trunk/email2trac.py.in @ 599

Last change on this file since 599 was 599, checked in by bas, 12 years ago

skip trac default setting for resolution can only be set in email2trac.conf, closes #299

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