source: trunk/email2trac.py.in @ 593

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

Changed license to apache version 2.0

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