source: trunk/email2trac.py.in @ 597

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

renamed function set_reply_fields to set_cc_fields. So we can use it in ticket_update and resolved a bug that reporter email address was not stripped from CC fields, see #293

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