source: trunk/email2trac.py.in @ 605

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

removed some obsolete code

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