source: trunk/email2trac.py.in @ 603

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

fixed an error in saving attachments on windows platformss, closes #300

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