source: trunk/email2trac.py.in @ 620

Last change on this file since 620 was 620, checked in by bas, 11 years ago

fixed a UnicodeDecode? Error in printing filename
added pseudo code for pip and virtual env

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