source: trunk/email2trac.py.in @ 640

Last change on this file since 640 was 640, checked in by bas, 10 years ago

intermediaite release

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 90.5 KB
Line 
1#!@PYTHON@
2#
3# This file is part of the email2trac utils
4#
5#       Copyright 2002 SURFsara
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 <bas.vandervlies@surfsara.nl>
32  Walter de Jong <walter.dejong@surfsara.nl>
33
34How to use
35----------
36 * See https://oss.trac.surfsara.nl/email2trac/
37
38 * Commandline opions:
39                -h,--help
40                -A, --agilo
41                -d, --debug
42                -E, --virtualenv <path>
43                -f,--file  <configuration file>
44                -n,--dry-run
45                -p, --project <project name>
46                -t, --ticket_prefix <name>
47                -v, --verbose
48
49SVN Info:
50        $Id: email2trac.py.in 640 2013-08-15 14:31:13Z bas $
51"""
52import os
53import sys
54import string
55import getopt
56import time
57import email
58import email.Iterators
59import email.Header
60import re
61import urllib
62import unicodedata
63import mimetypes
64import traceback
65import logging
66import logging.handlers
67import UserDict
68import tempfile
69
70from datetime import timedelta, datetime
71from stat import *
72
73## Some global variables
74#
75m = None
76
77class SaraDict(UserDict.UserDict):
78    def __init__(self, dictin = None):
79        UserDict.UserDict.__init__(self)
80        self.name = None
81       
82        if dictin:
83            if dictin.has_key('name'):
84                self.name = dictin['name']
85                del dictin['name']
86            self.data = dictin
87           
88    def get_value(self, name):
89        if self.has_key(name):
90            return self[name]
91        else:
92            return None
93               
94    def __repr__(self):
95        return repr(self.data)
96
97    def __str__(self):
98        return str(self.data)
99           
100    def __getattr__(self, name):
101        """
102        override the class attribute get method. Return the value
103        from the dictionary
104        """
105        if self.data.has_key(name):
106            return self.data[name]
107        else:
108            return None
109           
110    def __setattr__(self, name, value):
111        """
112        override the class attribute set method only when the UserDict
113        has set its class attribute
114        """
115        if self.__dict__.has_key('data'):
116            self.data[name] = value
117        else:
118            self.__dict__[name] = value
119
120    def __iter__(self):
121        return iter(self.data.keys())
122
123
124
125class TicketEmailParser(object):
126    env = None
127    comment = '> '
128
129    def __init__(self, env, parameters, logger, version):
130        self.env = env
131
132        # Database connection
133        #
134        self.db = None
135
136        # Save parameters
137        #
138        self.parameters = parameters
139        self.logger = logger
140
141        # Some useful mail constants
142        #
143        self.email_name = None
144        self.email_addr = None
145        self.email_from = None
146        self.author     = None
147        self.id         = None
148
149        ## Will be set to True if the user has ben registered
150        #
151        self.allow_registered_user = False
152       
153        self.STRIP_CONTENT_TYPES = list()
154
155        ## fields properties via body_text
156        #
157        self.properties = dict()
158
159        self.VERSION = version
160
161        self.get_config = self.env.config.get
162
163        ## init function ##
164        #
165        self.setup_parameters()
166
167    def setup_parameters(self):
168        if self.parameters.umask:
169            os.umask(self.parameters.umask)
170
171        if not self.parameters.spam_level:
172            self.parameters.spam_level = 0
173
174        if not self.parameters.spam_header:
175            self.parameters.spam_header = 'X-Spam-Score'
176
177        if not self.parameters.email_quote:
178            self.parameters.email_quote = '^> .*'
179            self.parameters.skip_line_regex = '^> .*'
180
181        if not self.parameters.ticket_update_by_subject_lookback:
182            self.parameters.ticket_update_by_subject_lookback = 30
183
184        if self.parameters.verbatim_format == None:
185            self.parameters.verbatim_format = 1
186
187        if self.parameters.reflow == None:
188            self.parameters.reflow = 1
189
190        if self.parameters.binhex:
191            self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
192
193        if self.parameters.applesingle:
194            self.STRIP_CONTENT_TYPES.append('application/applefile')
195
196        if self.parameters.appledouble:
197            self.STRIP_CONTENT_TYPES.append('application/applefile')
198
199        if self.parameters.strip_content_types:
200            items = self.parameters.strip_content_types.split(',')
201            for item in items:
202                self.STRIP_CONTENT_TYPES.append(item.strip())
203
204        if self.parameters.tmpdir:
205            self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
206        else:
207            self.parameters.tmpdir = os.path.normcase('/tmp')
208
209        if self.parameters.email_triggers_workflow == None:
210            self.parameters.email_triggers_workflow = 1
211
212        if not self.parameters.subject_field_separator:
213            self.parameters.subject_field_separator = '&'
214        else:
215            self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
216
217        if not self.parameters.strip_signature_regex:
218            self.parameters.strip_signature_regex = '^-- $'
219            self.parameters.cut_line_regex = '^-- $'
220
221        self.trac_smtp_from = self.get_config('notification', 'smtp_from')
222        self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
223        self.smtp_replyto = self.get_config('notification', 'smtp_replyto')
224        self.trac_smtp_always_cc = self.get_config('notification', 'smtp_always_cc')
225        self.trac_smtp_always_bcc = self.get_config('notification', 'smtp_always_bcc')
226
227        self.system = None
228
229########## Email Header Functions ###########################################################
230
231    def spam(self, message):
232        """
233        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
234        # Note if Spam_level then '*' are included
235        """
236        self.logger.debug('function spam')
237
238        spam = False
239        if message.has_key(self.parameters.spam_header):
240            spam_l = string.split(message[self.parameters.spam_header])
241            #self.logger.info('Spam header: %s' %(message[self.parameters.spam_header]))
242
243            try:
244                number = spam_l[0].count('*')
245            except IndexError, detail:
246                number = 0
247               
248            if number >= self.parameters.spam_level:
249                spam = True
250               
251        ## treat virus mails as spam
252        #
253        elif message.has_key('X-Virus-found'):         
254            spam = True
255
256        ## Astaro firewall spam handling
257        #
258        elif message.get('X-Spam-Flag') == "YES" and message.get('X-Spam-Result') == "Spam":
259            spam = True
260
261        ## How to handle SPAM messages
262        #
263        if self.parameters.drop_spam and spam:
264
265            self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
266            return 'drop'   
267
268        elif spam:
269
270            return 'Spam'   
271        else:
272
273            return False
274
275    def email_header_acl(self, keyword, message_field, default):
276        """
277        This function wil check if the email address is allowed or denied
278        to send mail to the ticket list
279        """
280        self.logger.debug('function email_header_acl: %s' %keyword)
281
282        try:
283            mail_addresses = self.parameters[keyword]
284
285            # Check if we have an empty string
286            #
287            if not mail_addresses:
288                return default
289
290        except KeyError, detail:
291            self.logger.debug('\t %s not defined, all messages are allowed.' %(keyword))
292
293            return default
294
295        mail_addresses = string.split(mail_addresses, ',')
296        message_addresses = string.split(message_field, ',')
297
298        for entry in mail_addresses:
299            entry = entry.strip()
300            TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
301
302            for addr in message_addresses:
303                addr = addr.strip()
304
305                if self.parameters.compare_function_list in [ 'matches', 'match']:
306                    s = '\t%s matches %s' %(addr, entry)
307                    result =  TO_RE.match(addr)
308                else:
309                    s = '\t%s contains %s' %(addr, entry)
310                    result =  TO_RE.search(addr)
311
312                if result:
313                    self.logger.debug(s)
314                    return True
315
316        return False
317
318    def email_header_txt(self, m):
319        """
320        Display To and CC addresses in description field
321        """
322        s = ''
323
324        if m['To'] and len(m['To']) > 0:
325            s = "'''To:''' %s\r\n" %(m['To'])
326        if m['Cc'] and len(m['Cc']) > 0:
327            s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
328
329        return  self.email_to_unicode(s)
330
331
332    def get_sender_info(self, message):
333        """
334        Get the default author name and email address from the message
335        """
336        self.logger.debug('function get_sender_info')
337
338        to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
339        self.email_to_addrs = list()
340
341        for n,e in to_addrs:
342            self.email_to_addrs.append(e)
343
344        self.email_to_addrs = ','.join(self.email_to_addrs)
345
346
347        self.email_name, self.email_addr  = email.Utils.parseaddr(message['from'])
348
349        ## decode email name can contain charset
350        #
351        self.email_name = self.email_to_unicode(self.email_name)
352
353        dstr = '\t email name: %s, email address: %s' %(self.email_name, self.email_addr) 
354        self.logger.debug(dstr)
355
356        ## Trac can not handle author's name that contains spaces
357        #
358        if self.email_addr.lower() == self.trac_smtp_from.lower():
359            if self.email_name:
360                self.author = self.email_name
361            else:
362                self.author = "email2trac"
363        else:
364            self.author = self.email_addr
365
366        if self.parameters.ignore_trac_user_settings:
367            return
368
369        # Is this a registered user, use email address as search key:
370        # result:
371        #   u : login name
372        #   n : Name that the user has set in the settings tab
373        #   e : email address that the user has set in the settings tab
374        #
375        users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
376            if (
377                (e and (e.lower() == self.email_addr.lower())) or
378                (u.lower() + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
379            )
380            ]
381
382        if len(users) >= 1:
383            self.email_from = users[0][2]
384            self.author = users[0][0]
385
386            self.logger.debug('\tget_sender_info: found registered user: %s' %(self.author) )
387
388            if self.parameters.white_list_registered_users:
389                self.logger.debug('\tget_sender_info: added registered user to white list')
390                self.allow_registered_user = True
391                   
392
393    def set_cc_fields(self, ticket, message, update=False):
394        """
395        Set all the right fields for a new ticket
396        """
397        self.logger.debug('function set_cc_fields')
398
399
400        # Put all CC-addresses in ticket CC field
401        #
402        if self.parameters.reply_all:
403
404            ticket_cc = ''
405
406            msg_cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
407
408            if not msg_cc_addrs:
409                return
410
411            if update:
412                self.logger.debug("\tupdate ticket cc-field")
413                ticket_cc = ticket['cc']
414                ticket_cc_list = ticket_cc.split(',')
415
416            for name,addr in msg_cc_addrs:
417       
418                ## Prevent mail loop
419                #
420                if addr == self.trac_smtp_from:
421                    self.logger.debug("\tSkipping %s email address for CC-field, same as smtp_from address in trac.ini " %(addr))
422                    continue
423
424                ## Alwyas remove reporter email address
425                #
426                elif addr == self.email_addr:
427                    self.logger.debug("\tSkipping reporter email address for CC-field")
428                    continue
429
430                ## Always remove the always_cc address
431                #
432                elif addr == self.trac_smtp_always_cc:
433                    self.logger.debug("\tSkipping smtp_always_cc email address for CC-field")
434                    continue
435
436                ## Always remove the always_bcc address
437                #
438                elif addr == self.trac_smtp_always_bcc:
439                    self.logger.debug("\tSkipping smtp_always_bcc email address for CC-field")
440                    continue
441
442                else:
443                    ## On update, prevent adding duplicates
444                    #
445                    if update:
446                      if addr in ticket_cc_list:
447                          continue
448                   
449                    if ticket_cc:
450                        ticket_cc = '%s, %s' %(ticket_cc, addr)
451                    else:
452                        ticket_cc = addr
453
454            if ticket_cc:
455                self.logger.debug('\tCC fields: %s' %ticket_cc)
456                ticket['cc'] = self.email_to_unicode(ticket_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        return (found_id, subject)
1214
1215
1216    def new_ticket(self, msg, subject, spam, set_fields = None):
1217        """
1218        Create a new ticket
1219        """
1220        self.logger.debug('function new_ticket')
1221
1222        tkt = Ticket(self.env)
1223
1224        ## self.author can be email address of an username
1225        #
1226        tkt['reporter'] = self.author
1227
1228        self.set_cc_fields(tkt, msg)
1229
1230        self.set_ticket_fields(tkt)
1231
1232        ## Check the permission of the reporter
1233        #
1234        if self.parameters.ticket_permission_system:
1235            if not self.check_permission(tkt, 'TICKET_CREATE'):
1236                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1237                return False
1238
1239        ## Old style setting for component, will be removed
1240        #
1241        if spam:
1242            tkt['component'] = 'Spam'
1243
1244        elif self.parameters.has_key('component'):
1245            tkt['component'] = self.parameters['component']
1246
1247        if not msg['Subject']:
1248            tkt['summary'] = u'(No subject)'
1249        else:
1250            tkt['summary'] = subject
1251
1252
1253        if set_fields:
1254            rest, keywords = string.split(set_fields, '?')
1255
1256            if keywords:
1257                update_fields = self.str_to_dict(keywords)
1258                self.update_ticket_fields(tkt, update_fields)
1259
1260
1261        message_parts = self.get_message_parts(msg, True)
1262
1263        ## Must we update some ticket fields properties via body_text
1264        #
1265        if self.properties:
1266                self.update_ticket_fields(tkt, self.properties)
1267
1268        message_parts = self.unique_attachment_names(message_parts)
1269       
1270        ## produce e-mail like header
1271        #
1272        head = ''
1273        if self.parameters.email_header:
1274            head = self.email_header_txt(msg)
1275            message_parts.insert(0, head)
1276           
1277        body_text = self.get_body_text(message_parts)
1278
1279        tkt['description'] = body_text
1280
1281        ## When is the change committed
1282        #
1283        if self.VERSION < 0.11:
1284            when = int(time.time())
1285        else:
1286            when = datetime.now(util.datefmt.utc)
1287
1288        if self.parameters.dry_run:
1289            self.logger.info('DRY_RUN: tkt.insert()')
1290        else:
1291            self.id = tkt.insert()
1292   
1293        changed = False
1294        comment = ''
1295
1296        ## some routines in trac are dependend on ticket id
1297        #  like alternate notify template
1298        #
1299        if self.parameters.alternate_notify_template:
1300            tkt['id'] = self.id
1301            changed = True
1302
1303        ## Rewrite the description if we have mailto enabled
1304        #
1305        if self.parameters.mailto_link:
1306            changed = True
1307            comment = u'\nadded mailto line\n'
1308
1309            #mailto = self.html_mailto_link( m['Subject'])
1310            mailto = self.html_mailto_link(subject)
1311
1312            tkt['description'] = u'%s\r\n%s%s\r\n' \
1313                %(head, mailto, body_text)
1314   
1315        ## Save the attachments to the ticket   
1316        #
1317        error_with_attachments =  self.attach_attachments(message_parts)
1318
1319        if error_with_attachments:
1320            changed = True
1321            comment = '%s\n%s\n' %(comment, error_with_attachments)
1322
1323        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
1324            if self.mail_workflow(tkt):
1325                changed = True
1326
1327        if changed:
1328            if self.parameters.dry_run:
1329                s = 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1330                self.logger.info(s)
1331
1332            else:
1333                tkt.save_changes(tkt['reporter'], comment)
1334
1335        if not spam:
1336            self.notify(tkt, True)
1337
1338
1339    def attach_attachments(self, message_parts, update=False):
1340        '''
1341        save any attachments as files in the ticket's directory
1342        '''
1343        self.logger.debug('function attach_attachments()')
1344
1345        if self.parameters.dry_run:
1346            self.logger.debug("DRY_RUN: no attachments attached to tickets")
1347            return ''
1348
1349        count = 0
1350
1351        ## Get Maxium attachment size
1352        #
1353        max_size = int(self.get_config('attachment', 'max_size'))
1354        status   = None
1355       
1356        for item in message_parts:
1357            ## Skip body parts
1358            #
1359            if not isinstance(item, tuple):
1360                continue
1361               
1362            (original, filename, part) = item
1363
1364            ## We have to determine the size so we use this temporary solution.
1365            #
1366            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, 'email2trac_tmp.att'))
1367            text = part.get_payload(decode=1)
1368            if not text:
1369                text = '(None)'
1370            fd.write(text)
1371            fd.close()
1372
1373            ## get the file_size
1374            #
1375            stats = os.lstat(path)
1376            file_size = stats[ST_SIZE]
1377
1378            ## Check if the attachment size is allowed
1379            #
1380            if (max_size != -1) and (file_size > max_size):
1381                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1382                    %(status, original, file_size, max_size)
1383
1384                os.unlink(path)
1385                continue
1386            else:
1387                count = count + 1
1388                   
1389            ## Insert the attachment
1390            #
1391            fd = open(path, 'rb')
1392            if self.system == 'discussion':
1393                att = attachment.Attachment(self.env, 'discussion', 'topic/%s' % (self.id,))
1394            elif self.system == 'blog':
1395                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,))
1396            else:
1397                s = 'Attach %s to ticket %d' %(filename, self.id)
1398                self.print_unicode(s)
1399                att = attachment.Attachment(self.env, 'ticket', self.id)
1400 
1401            ## This will break the ticket_update system, the body_text is vaporized
1402            #  ;-(
1403            #
1404            if not update:
1405                att.author = self.author
1406                att.description = self.email_to_unicode('Added by email2trac')
1407
1408            try:
1409
1410                self.logger.debug('Insert atachment')
1411                att.insert(filename, fd, file_size)
1412
1413            except OSError, detail:
1414
1415                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1416                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
1417
1418            ## Remove the created temporary filename
1419            #
1420            fd.close()
1421            os.unlink(path)
1422
1423        ## return error
1424        #
1425        return status
1426
1427########## Fullblog functions  #################################################
1428
1429    def blog(self, msg, subject, id, params):
1430        """
1431        The blog create/update function
1432        """
1433        ## import the modules
1434        #
1435        from tracfullblog.core import FullBlogCore
1436        from tracfullblog.model import BlogPost, BlogComment
1437
1438        ## instantiate blog core
1439        #
1440        blog = FullBlogCore(self.env)
1441        req = Mock(authname='anonymous', perm=MockPerm(), args={})
1442
1443        ## parameters from the subject
1444        #
1445        params = self.str_to_dict((params or '').lstrip('?'))
1446
1447        ## preferably get the time from the email date header, or else
1448        #  use current date and time
1449        date = email.Utils.parsedate_tz(msg.get('date'))
1450        if date:
1451            dt = util.datefmt.to_datetime(email.Utils.mktime_tz(date), util.datefmt.utc)
1452        else:
1453            self.logger.warn("No valid date header found")
1454            dt = util.datefmt.to_datetime(None, util.datefmt.utc)
1455
1456        ## blog entry affected
1457        #
1458        self.id = id or util.datefmt.format_datetime(dt, "%Y%m%d%H%M%S", util.datefmt.utc)
1459
1460        ## check wether a blog post exists
1461        #
1462        post = BlogPost(self.env, self.id)
1463        force_update = self.properties.get('update', params.get('update'))
1464
1465        ## message parts
1466        #
1467        message_parts = self.get_message_parts(msg)
1468        message_parts = self.unique_attachment_names(message_parts)
1469
1470        if post.get_versions() and not force_update:
1471
1472            ## add comment to blog entry
1473            #
1474            comment = BlogComment(self.env, self.id)
1475            comment.author = self.properties.get('author', params.get('author', self.author))
1476            comment.comment = self.get_body_text(message_parts)
1477            comment.time = dt
1478
1479            if self.parameters.dry_run:
1480                self.logger.info('DRY-RUN: not adding comment for blog entry "%s"' % id)
1481                return
1482            warnings = blog.create_comment(req, comment)
1483
1484        else:
1485            ## create or update blog entry
1486            #
1487            post.author = self.properties.get('author', params.get('author', self.author))
1488            post.categories = self.properties.get('categories', params.get('categories', ''))
1489            post.title = subject.strip()
1490            post.publish_time = dt
1491            post.body = self.get_body_text(message_parts)
1492           
1493            if self.parameters.dry_run:
1494                self.logger.info('DRY-RUN: not creating blog entry "%s"' % post.title)
1495                return
1496            warnings = blog.create_post(req, post, self.author, u'Created by email2trac', False)
1497
1498        ## check for problems
1499        #
1500        if warnings:
1501            raise TracError(', '.join('blog:%s:%s' % (w[0], w[1]) for w in warnings))
1502       
1503        ## all seems well, attach attachments
1504        #
1505        self.attach_attachments(message_parts)
1506
1507
1508########## Discussion functions  ##############################################
1509
1510    def discussion_topic(self, content, subject):
1511
1512        ## Import modules.
1513        #
1514        from tracdiscussion.api import DiscussionApi
1515        from trac.util.datefmt import to_timestamp, utc
1516
1517        self.logger.debug('Creating a new topic in forum:', self.id)
1518
1519        ## Get dissussion API component.
1520        #
1521        api = self.env[DiscussionApi]
1522        context = self._create_context(content, subject)
1523
1524        ## Get forum for new topic.
1525        #
1526        forum = api.get_forum(context, self.id)
1527
1528        if not forum:
1529            self.logger.error("ERROR: Replied forum doesn't exist")
1530
1531        ## Prepare topic.
1532        #
1533        topic = {'forum' : forum['id'],
1534                 'subject' : context.subject,
1535                 'time': to_timestamp(datetime.now(utc)),
1536                 'author' : self.author,
1537                 'subscribers' : [self.email_addr],
1538                 'body' : self.get_body_text(context.content_parts)}
1539
1540        ## Add topic to DB and commit it.
1541        #
1542        self._add_topic(api, context, topic)
1543        self.db.commit()
1544
1545    def discussion_topic_reply(self, content, subject):
1546
1547        ## Import modules.
1548        #
1549        from tracdiscussion.api import DiscussionApi
1550        from trac.util.datefmt import to_timestamp, utc
1551
1552        self.logger.debug('Replying to discussion topic', self.id)
1553
1554        ## Get dissussion API component.
1555        #
1556        api = self.env[DiscussionApi]
1557        context = self._create_context(content, subject)
1558
1559        ## Get replied topic.
1560        #
1561        topic = api.get_topic(context, self.id)
1562
1563        if not topic:
1564            self.logger.error("ERROR: Replied topic doesn't exist")
1565
1566        ## Prepare message.
1567        #
1568        message = {'forum' : topic['forum'],
1569                   'topic' : topic['id'],
1570                   'replyto' : -1,
1571                   'time' : to_timestamp(datetime.now(utc)),
1572                   'author' : self.author,
1573                   'body' : self.get_body_text(context.content_parts)}
1574
1575        ## Add message to DB and commit it.
1576        #
1577        self._add_message(api, context, message)
1578        self.db.commit()
1579
1580    def discussion_message_reply(self, content, subject):
1581
1582        ## Import modules.
1583        #
1584        from tracdiscussion.api import DiscussionApi
1585        from trac.util.datefmt import to_timestamp, utc
1586
1587        self.logger.debug('Replying to discussion message', self.id)
1588
1589        ## Get dissussion API component.
1590        #
1591        api = self.env[DiscussionApi]
1592        context = self._create_context(content, subject)
1593
1594        ## Get replied message.
1595        #
1596        message = api.get_message(context, self.id)
1597
1598        if not message:
1599            self.logger.error("ERROR: Replied message doesn't exist")
1600
1601        ## Prepare message.
1602        #
1603        message = {'forum' : message['forum'],
1604                   'topic' : message['topic'],
1605                   'replyto' : message['id'],
1606                   'time' : to_timestamp(datetime.now(utc)),
1607                   'author' : self.author,
1608                   'body' : self.get_body_text(context.content_parts)}
1609
1610        ## Add message to DB and commit it.
1611        #
1612        self._add_message(api, context, message)
1613        self.db.commit()
1614
1615    def _create_context(self, content, subject):
1616
1617        ## Import modules.
1618        #
1619        from trac.mimeview import Context
1620        from trac.web.api import Request
1621        from trac.perm import PermissionCache
1622
1623        ## TODO: Read server base URL from config.
1624        #  Create request object to mockup context creation.
1625        #
1626        environ = {'SERVER_PORT' : 80,
1627                   'SERVER_NAME' : 'test',
1628                   'REQUEST_METHOD' : 'POST',
1629                   'wsgi.url_scheme' : 'http',
1630                   'wsgi.input' : sys.stdin}
1631        chrome =  {'links': {},
1632                   'scripts': [],
1633                   'ctxtnav': [],
1634                   'warnings': [],
1635                   'notices': []}
1636
1637        if self.env.base_url_for_redirect:
1638            environ['trac.base_url'] = self.env.base_url
1639
1640        req = Request(environ, None)
1641        req.chrome = chrome
1642        req.tz = 'missing'
1643        req.authname = self.author
1644        req.perm = PermissionCache(self.env, self.author)
1645        req.locale = None
1646
1647        ## Create and return context.
1648        #
1649        context = Context.from_request(req)
1650        context.realm = 'discussion-email2trac'
1651        context.cursor = self.db.cursor()
1652        context.content = content
1653        context.subject = subject
1654
1655        ## Read content parts from content.
1656        #
1657        context.content_parts = self.get_message_parts(content)
1658        context.content_parts = self.unique_attachment_names(
1659          context.content_parts)
1660
1661        return context
1662
1663    def _add_topic(self, api, context, topic):
1664        context.req.perm.assert_permission('DISCUSSION_APPEND')
1665
1666        ## Filter topic.
1667        #
1668        for discussion_filter in api.discussion_filters:
1669            accept, topic_or_error = discussion_filter.filter_topic(
1670              context, topic)
1671            if accept:
1672                topic = topic_or_error
1673            else:
1674                raise TracError(topic_or_error)
1675
1676        ## Add a new topic.
1677        #
1678        api.add_topic(context, topic)
1679
1680        ## Get inserted topic with new ID.
1681        #
1682        topic = api.get_topic_by_time(context, topic['time'])
1683
1684        ## Attach attachments.
1685        #
1686        self.id = topic['id']
1687        self.attach_attachments(context.content_parts, True)
1688
1689        ## Notify change listeners.
1690        #
1691        for listener in api.topic_change_listeners:
1692            listener.topic_created(context, topic)
1693
1694    def _add_message(self, api, context, message):
1695        context.req.perm.assert_permission('DISCUSSION_APPEND')
1696
1697        ## Filter message.
1698        #
1699        for discussion_filter in api.discussion_filters:
1700            accept, message_or_error = discussion_filter.filter_message(
1701              context, message)
1702            if accept:
1703                message = message_or_error
1704            else:
1705                raise TracError(message_or_error)
1706
1707        ## Add message.
1708        #
1709        api.add_message(context, message)
1710
1711        ## Get inserted message with new ID.
1712        #
1713        message = api.get_message_by_time(context, message['time'])
1714
1715        ## Attach attachments.
1716        #
1717
1718        self.attach_attachments(context.content_parts, True)
1719
1720        ## Notify change listeners.
1721        #
1722        for listener in api.message_change_listeners:
1723            listener.message_created(context, message)
1724
1725########## MAIN function  ######################################################
1726
1727    def parse(self, fp):
1728        """
1729        """
1730        self.logger.debug('Main function parse')
1731        global m
1732
1733        m = email.message_from_file(fp)
1734       
1735        if not m:
1736            self.logger.debug('This is not a valid email message format')
1737            return
1738           
1739        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1740        #
1741        try:
1742            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1743        except AttributeError, detail:
1744            pass
1745
1746        if self.parameters.debug:     # save the entire e-mail message text
1747            self.save_email_for_debug(m, self.parameters.project_name, True)
1748
1749        if self.VERSION <= 0.11:
1750            self.db = self.env.get_db_cnx()
1751        else:
1752            self.db = self.env.get_read_db()
1753
1754        self.get_sender_info(m)
1755
1756
1757        if self.parameters.white_list_file:
1758            self.acl_list_from_file(self.parameters.white_list_file, 'white_list')
1759
1760        if not ( self.email_header_acl('white_list', self.email_addr, True) or self.allow_registered_user ) :
1761
1762            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1763            return False
1764
1765        if self.email_header_acl('black_list', self.email_addr, False):
1766            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1767            return False
1768
1769        if not self.email_header_acl('recipient_list', self.email_to_addrs, True):
1770            self.logger.info('Message rejected : %s not in recipient list' %(self.email_to_addrs))
1771            return False
1772
1773        ## If spam drop the message
1774        #
1775        if self.spam(m) == 'drop':
1776            return False
1777
1778        elif self.spam(m) == 'spam':
1779            spam_msg = True
1780        else:
1781            spam_msg = False
1782
1783        if not m['Subject']:
1784            subject  = 'No Subject'
1785        else:
1786            subject  = self.email_to_unicode(m['Subject'])
1787
1788        ## Bug in python logging module <2.6
1789        #
1790        self.logger.info('subject: %s' %repr(subject))
1791
1792        ## First try the new method and then fall back to old method
1793        #
1794        if not self.parse_delivered_to_field(m, subject, spam_msg):
1795            self.parse_subject_field(m, subject, spam_msg)
1796
1797    def parse_delivered_to_field(self, m, subject, spam_msg):
1798        """
1799        """
1800        self.logger.debug('function parse_delivered_to_field')
1801
1802        ## Ticket id is in Delivered-To Field. Trac notify must send this
1803        #  eg: is+390@surfsara.nl
1804        try:
1805
1806            #print self.parameters.project_name
1807            self.logger.debug('Delivered To: %s' %m['Delivered-To'])
1808
1809            id = m['Delivered-To']
1810            id = id.split(self.parameters.recipient_delimiter)[1]
1811            id = id.split('@')[0]
1812
1813            self.logger.debug('\t Found ticket id: %s' %id)
1814
1815            if not self.ticket_update(m, id, spam_msg):
1816                return False
1817
1818        except KeyError, detail:
1819            pass
1820        except IndexError, detail:
1821            pass
1822
1823        return False
1824
1825    def parse_subject_field(self, m, subject, spam_msg):
1826        """
1827        """
1828        self.logger.debug('function parse_subject_header')
1829
1830        ## [hic] #1529: Re: LRZ
1831        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1832        #
1833        ticket_regex = r'''
1834            (?P<new_fields>[#][?].*)
1835            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1836            '''
1837
1838        ## Check if  FullBlogPlugin is installed
1839        #
1840        blog_enabled = None
1841        blog_regex = ''
1842        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1843            self.logger.debug('Trac BLOG support enabled')
1844            blog_enabled = True
1845            blog_regex = '''|(?P<blog>blog(?P<blog_params>[?][^:]*)?:(?P<blog_id>\S*))'''
1846
1847
1848        ## Check if DiscussionPlugin is installed
1849        #
1850        discussion_enabled = None
1851        discussion_regex = ''
1852        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1853            self.logger.debug('Trac Discussion support enabled')
1854            discussion_enabled = True
1855            discussion_regex = r'''
1856            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1857            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1858            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1859            '''
1860
1861
1862        regex_str = ticket_regex + blog_regex + discussion_regex
1863        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1864
1865        ## Find out if this is a ticket, a blog or a discussion
1866        #
1867        result =  SYSTEM_RE.search(subject)
1868
1869        if result:
1870            ## update ticket + fields
1871            #
1872            if result.group('reply'):
1873                self.system = 'ticket'
1874
1875                ## Skip the last ':' character
1876                #
1877                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1878                    self.new_ticket(m, subject, spam_msg)
1879
1880            ## New ticket + fields
1881            #
1882            elif result.group('new_fields'):
1883                self.system = 'ticket'
1884                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1885
1886            if blog_enabled:
1887                if result.group('blog'):
1888                    self.system = 'blog'
1889                    self.blog(m, subject[result.end('blog'):], result.group('blog_id'), result.group('blog_params'))
1890
1891            if discussion_enabled:
1892                ## New topic.
1893                #
1894                if result.group('forum'):
1895                    self.system = 'discussion'
1896                    self.id = int(result.group('forum_id'))
1897                    self.discussion_topic(m, subject[result.end('forum'):])
1898
1899                ## Reply to topic.
1900                #
1901                elif result.group('topic'):
1902                    self.system = 'discussion'
1903                    self.id = int(result.group('topic_id'))
1904                    self.discussion_topic_reply(m, subject[result.end('topic'):])
1905
1906                ## Reply to topic message.
1907                #
1908                elif result.group('message'):
1909                    self.system = 'discussion'
1910                    self.id = int(result.group('message_id'))
1911                    self.discussion_message_reply(m, subject[result.end('message'):])
1912
1913        else:
1914
1915            self.system = 'ticket'
1916            (matched_id, subject) = self.ticket_update_by_subject(subject)
1917
1918            if matched_id:
1919
1920                if not self.ticket_update(m, matched_id, spam_msg):
1921                    self.new_ticket(m, subject, spam_msg)
1922
1923            else:
1924                ## No update by subject, so just create a new ticket
1925                #
1926                self.new_ticket(m, subject, spam_msg)
1927
1928
1929########## BODY TEXT functions  ###########################################################
1930
1931    def strip_signature(self, text):
1932        """
1933        Strip signature from message, inspired by Mailman software
1934        """
1935        self.logger.debug('function strip_signature: %s' %self.parameters.strip_signature_regex)
1936
1937        body = []
1938
1939        STRIP_RE = re.compile( self.parameters.strip_signature_regex )
1940        for line in text.splitlines():
1941
1942            match = STRIP_RE.match(line)
1943            if match:
1944                self.logger.debug('\t"%s "  matched, skiping rest of message' %line)
1945                break
1946
1947            body.append(line)
1948
1949        return ('\n'.join(body))
1950
1951    def reflow(self, text, delsp = 0):
1952        """
1953        Reflow the message based on the format="flowed" specification (RFC 3676)
1954        """
1955        flowedlines = []
1956        quotelevel = 0
1957        prevflowed = 0
1958
1959        for line in text.splitlines():
1960            from re import match
1961           
1962            ## Figure out the quote level and the content of the current line
1963            #
1964            m = match('(>*)( ?)(.*)', line)
1965            linequotelevel = len(m.group(1))
1966            line = m.group(3)
1967
1968            ## Determine whether this line is flowed
1969            #
1970            if line and line != '-- ' and line[-1] == ' ':
1971                flowed = 1
1972            else:
1973                flowed = 0
1974
1975            if flowed and delsp and line and line[-1] == ' ':
1976                line = line[:-1]
1977
1978            ## If the previous line is flowed, append this line to it
1979            #
1980            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1981                flowedlines[-1] += line
1982
1983            ## Otherwise, start a new line
1984            #
1985            else:
1986                flowedlines.append('>' * linequotelevel + line)
1987
1988            prevflowed = flowed
1989           
1990
1991        return '\n'.join(flowedlines)
1992
1993    def strip_quotes(self, text):
1994        """
1995        Strip quotes from message by Nicolas Mendoza
1996        """
1997        self.logger.debug('function strip_quotes: %s' %self.parameters.email_quote)
1998
1999        body = []
2000
2001        STRIP_RE = re.compile( self.parameters.email_quote )
2002
2003        for line in text.splitlines():
2004
2005            try:
2006
2007                match = STRIP_RE.match(line)
2008                if match:
2009                    self.logger.debug('\t"%s "  matched, skipping rest of message' %line)
2010                    continue
2011
2012            except UnicodeDecodeError:
2013
2014                tmp_line = self.email_to_unicode(line)
2015
2016                match = STRIP_RE.match(tmp_line)
2017                if match:
2018                    self.logger.debug('\t"%s "  matched, skipping rest of message' %line)
2019                    continue
2020               
2021            body.append(line)
2022
2023        return ('\n'.join(body))
2024
2025    def inline_properties(self, text):
2026        """
2027        Parse text if we use inline keywords to set ticket fields
2028        """
2029        self.logger.debug('function inline_properties')
2030
2031        properties = dict()
2032        body = list()
2033
2034        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
2035
2036        for line in text.splitlines():
2037            match = INLINE_EXP.match(line)
2038            if match:
2039                keyword, value = match.groups()
2040
2041                if self.parameters.inline_properties_first_wins:
2042                    if keyword in self.properties.keys():
2043                        continue
2044
2045                self.properties[keyword] = value.strip()
2046                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
2047
2048            else:
2049                body.append(line)
2050               
2051        return '\n'.join(body)
2052
2053
2054    def wrap_text(self, text, replace_whitespace = False):
2055        """
2056        Will break a lines longer then given length into several small
2057        lines of size given length
2058        """
2059        import textwrap
2060
2061        LINESEPARATOR = '\n'
2062        reformat = ''
2063
2064        for s in text.split(LINESEPARATOR):
2065            tmp = textwrap.fill(s, self.parameters.use_textwrap)
2066            if tmp:
2067                reformat = '%s\n%s' %(reformat,tmp)
2068            else:
2069                reformat = '%s\n' %reformat
2070
2071        return reformat
2072
2073        # Python2.4 and higher
2074        #
2075        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
2076        #
2077
2078########## EMAIL attachements functions ###########################################################
2079
2080    def inline_part(self, part):
2081        """
2082        """
2083        self.logger.debug('function inline_part()')
2084
2085        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
2086
2087    def get_message_parts(self, msg, new_email=False):
2088        """
2089        parses the email message and returns a list of body parts and attachments
2090        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
2091        """
2092        self.logger.debug('function get_message_parts()')
2093
2094        message_parts = list()
2095   
2096        ALTERNATIVE_MULTIPART = False
2097
2098        for part in msg.walk():
2099            content_maintype = part.get_content_maintype()
2100            content_type =  part.get_content_type()
2101
2102            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
2103            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
2104
2105            ## Check content type
2106            #
2107            if content_type in self.STRIP_CONTENT_TYPES:
2108                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
2109                continue
2110
2111            ## Catch some mulitpart execptions
2112            #
2113            if content_type == 'multipart/alternative':
2114                ALTERNATIVE_MULTIPART = True
2115                continue
2116
2117            ## Skip multipart containers
2118            #
2119            if content_maintype == 'multipart':
2120                self.logger.debug("\t Skipping multipart container")
2121                continue
2122           
2123            ## Check if this is an inline part. It's inline if there is co Cont-Disp header,
2124            #  or if there is one and it says "inline"
2125            #
2126            inline = self.inline_part(part)
2127
2128            ## Drop HTML message
2129            #
2130            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
2131
2132                if content_type == 'text/html':
2133                    self.logger.debug('\t Skipping alternative HTML message')
2134                    ALTERNATIVE_MULTIPART = False
2135                    continue
2136
2137            filename = part.get_filename()
2138            s = '\t unicode filename: %s' %(filename)
2139            self.print_unicode(s)
2140            self.logger.debug('\t raw filename: %s' %repr(filename))
2141
2142            filename = self.check_filename_length(filename)
2143
2144            ## Save all non plain text message as attachment
2145            #
2146            if not content_type in ['text/plain']:
2147
2148                message_parts.append( (filename, part) )
2149
2150                ## We only convert html messages
2151                #
2152                if not content_type == 'text/html':
2153                    self.logger.debug('\t Appending %s (%s)' %(repr(filename), content_type))
2154                    continue
2155
2156
2157            ## We have an text or html message
2158            #
2159            if not inline:
2160                    self.logger.debug('\t Appending %s (%s), not an inline messsage part' %(repr(filename), content_type))
2161                    message_parts.append( (filename, part) )
2162                    continue
2163               
2164            ## Try to decode message part. We have a html or plain text messafe
2165            #
2166            body_text = part.get_payload(decode=1)
2167            if not body_text:           
2168                body_text = part.get_payload(decode=0)
2169
2170            ## Try to convert html message
2171            #
2172            if content_type == 'text/html':
2173
2174                body_text = self.html_2_txt(body_text)
2175                if not body_text:
2176                    continue
2177
2178            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
2179            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
2180
2181            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
2182                body_text = self.reflow(body_text, delsp == 'yes')
2183
2184            if new_email and self.parameters.only_strip_on_update:
2185                self.logger.debug('Skip signature/quote stripping for new messages')
2186            else:
2187                if self.parameters.strip_signature:
2188                    body_text = self.strip_signature(body_text)
2189
2190                if self.parameters.strip_quotes:
2191                    body_text = self.strip_quotes(body_text)
2192
2193            if self.parameters.inline_properties:
2194                body_text = self.inline_properties(body_text)
2195
2196            if self.parameters.use_textwrap:
2197                body_text = self.wrap_text(body_text)
2198
2199            ## Get contents charset (iso-8859-15 if not defined in mail headers)
2200            #
2201            charset = part.get_content_charset()
2202            if not charset:
2203                charset = 'iso-8859-15'
2204
2205            try:
2206                ubody_text = unicode(body_text, charset)
2207
2208            except UnicodeError, detail:
2209                ubody_text = unicode(body_text, 'iso-8859-15')
2210
2211            except LookupError, detail:
2212                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
2213
2214            if self.parameters.verbatim_format:
2215                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
2216            else:
2217                message_parts.append('%s' %ubody_text)
2218
2219        return message_parts
2220       
2221    def unique_attachment_names(self, message_parts):
2222        """
2223        Make sure we have unique names attachments:
2224          - check if it contains illegal characters
2225          - Rename "None" filenames to "untitled-part"
2226        """
2227        self.logger.debug('function unique_attachment_names()')
2228        renamed_parts = []
2229        attachment_names = set()
2230
2231        for item in message_parts:
2232           
2233            ## If not an attachment, leave it alone
2234            #
2235            if not isinstance(item, tuple):
2236                renamed_parts.append(item)
2237                continue
2238               
2239            (filename, part) = item
2240
2241            ## If filename = None, use a default one
2242            #
2243            if filename in [ 'None']:
2244                filename = 'untitled-part'
2245                self.logger.info('\t Rename filename "None" to: %s' %filename)
2246
2247                ## Guess the extension from the content type, use non strict mode
2248                #  some additional non-standard but commonly used MIME types
2249                #  are also recognized
2250                #
2251                ext = mimetypes.guess_extension(part.get_content_type(), False)
2252                if not ext:
2253                    ext = '.bin'
2254
2255                filename = '%s%s' % (filename, ext)
2256
2257            ## Discard relative paths for windows/unix in attachment names
2258            #
2259            filename = filename.replace('\\', '_')
2260            filename = filename.replace('/', '_')
2261
2262            ## remove linefeed char
2263            #
2264            for forbidden_char in ['\r', '\n']:
2265                filename = filename.replace(forbidden_char,'')
2266
2267            ## We try to normalize the filename to utf-8 NFC if we can.
2268            #  Files uploaded from OS X might be in NFD.
2269            #  Check python version and then try it
2270            #
2271            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2272            #   try:
2273            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2274            #   except TypeError:
2275            #       pass
2276
2277            ## Make the filename unique for this ticket
2278            #
2279            num = 0
2280            unique_filename = filename
2281            dummy_filename, ext = os.path.splitext(filename)
2282
2283            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2284                num += 1
2285                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2286               
2287            s = '\t Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2288            self.print_unicode(s)
2289
2290            attachment_names.add(unique_filename)
2291
2292            renamed_parts.append((filename, unique_filename, part))
2293   
2294        return renamed_parts
2295           
2296           
2297    def attachment_exists(self, filename):
2298
2299        self.logger.debug("function attachment_exists")
2300
2301        s = '\t check if attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2302        self.print_unicode(s)
2303
2304        ## Do we have a valid ticket id
2305        #
2306        if not self.id:
2307            return False
2308
2309        try:
2310            if self.system == 'discussion':
2311
2312                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s' % (self.id,), filename)
2313
2314            elif self.system == 'blog':
2315
2316                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,), filename)
2317
2318            else:
2319
2320                att = attachment.Attachment(self.env, 'ticket', self.id, filename)
2321
2322            return True
2323
2324        except attachment.ResourceNotFound:
2325
2326            return False
2327
2328########## TRAC Ticket Text ###########################################################
2329           
2330    def get_body_text(self, message_parts):
2331        """
2332        """
2333        self.logger.debug('function get_body_text()')
2334
2335        body_text = []
2336       
2337        for part in message_parts:
2338       
2339            ## Plain text part, append it
2340            #
2341            if not isinstance(part, tuple):
2342                body_text.extend(part.strip().splitlines())
2343                body_text.append("")
2344                continue
2345
2346            (original, filename, part) = part
2347            inline = self.inline_part(part)
2348
2349            ## Skip generation of attachment link if html is converted to text
2350            #
2351            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2352                s = 'Skipping attachment link for html part: %s' %(filename)
2353                self.print_unicode(s)
2354                continue
2355           
2356            if part.get_content_maintype() == 'image' and inline:
2357
2358                if self.system != 'discussion':
2359                    s = 'wiki image link for: %s' %(filename)
2360                    self.print_unicode(s)
2361                    body_text.append('[[Image(%s)]]' % filename)
2362
2363                body_text.append("")
2364
2365            else:
2366
2367                if self.system != 'discussion':
2368
2369                    s = 'wiki attachment link for: %s' %(filename)
2370                    self.print_unicode(s)
2371                    body_text.append('[attachment:"%s"]' % filename)
2372
2373                body_text.append("")
2374
2375        ## Convert list body_texts to string
2376        #
2377        body_text = '\r\n'.join(body_text)
2378        return body_text
2379
2380    def html_mailto_link(self, subject):
2381        """
2382        This function returns a HTML mailto tag with the ticket id and author email address
2383        """
2384        self.logger.debug("function html_mailto_link")
2385
2386        if not self.author:
2387            author = self.email_addr
2388        else:   
2389            author = self.author
2390
2391        if not self.parameters.mailto_cc:
2392            self.parameters.mailto_cc = ''
2393           
2394        ## Bug in urllib.quote function
2395        #
2396        if isinstance(subject, unicode):
2397            subject = subject.encode('utf-8')
2398
2399        ## use urllib to escape the chars
2400        #
2401        s = '%s?Subject=%s&cc=%s' %(
2402               urllib.quote(self.email_addr),
2403               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2404               urllib.quote(self.parameters.mailto_cc)
2405               )
2406
2407        if self.VERSION in [ 0.10 ]:
2408            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)
2409        else:
2410            s = '[mailto:"%s" Reply to: %s]' %(s, author)
2411
2412        self.logger.debug("\tmailto link %s" %s)
2413        return s
2414
2415########## TRAC notify section ###########################################################
2416
2417    def notify(self, tkt, new=True, modtime=0):
2418        """
2419        A wrapper for the TRAC notify function. So we can use templates
2420        """
2421        self.logger.debug('function notify()')
2422
2423
2424        class Email2TracNotifyEmail(TicketNotifyEmail):
2425
2426            def __init__(self, env):
2427                TicketNotifyEmail.__init__(self, env)
2428                self.email2trac_notify_reporter = None
2429                self.email2trac_replyto = None
2430
2431            def send(self, torcpts, ccrcpts):
2432                #print 'Yes this works'
2433                dest = self.reporter or 'anonymous'
2434                hdrs = {}
2435                hdrs['Message-ID'] = self.get_message_id(dest, self.modtime)
2436                hdrs['X-Trac-Ticket-ID'] = str(self.ticket.id)
2437                hdrs['X-Trac-Ticket-URL'] = self.data['ticket']['link']
2438                if not self.newticket:
2439                    msgid = self.get_message_id(dest)
2440                    hdrs['In-Reply-To'] = msgid
2441                    hdrs['References'] = msgid
2442
2443
2444                if self.email2trac_notify_reporter:
2445                    if not self.email2trac_notify_reporter in torcpts:
2446                        torcpts.append(sender_email)
2447
2448                if self.email2trac_replyto:
2449                    # use to rewrite reply to
2450                    # hdrs does not work, multiple reply addresses
2451                    #hdrs['Reply-To'] = 'bas.van.der.vlies@gmail.com'
2452                    self.replyto_email = self.email2trac_replyto
2453       
2454                NotifyEmail.send(self, torcpts, ccrcpts, hdrs)
2455
2456        if self.parameters.dry_run  :
2457                self.logger.info('DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter'])
2458                return
2459        try:
2460
2461            ## create false {abs_}href properties, to trick Notify()
2462            #
2463            if self.VERSION in [0.10]:
2464                self.env.abs_href = Href(self.get_config('project', 'url'))
2465                self.env.href = Href(self.get_config('project', 'url'))
2466
2467            tn = Email2TracNotifyEmail(self.env)
2468
2469            ## additionally append sender (regardeless of settings in trac.ini)
2470            #
2471            if self.parameters.notify_reporter:
2472
2473                self.logger.debug('\t Notify reporter set')
2474
2475                if not self.email_header_acl('notify_reporter_black_list', self.email_addr, False):
2476                    tn.email2trac_notify_reporter = self.email_addr
2477
2478            if self.parameters.notify_replyto_rewrite:
2479
2480                self.logger.debug('\t Notify replyto rewrite set')
2481
2482                action, value = self.parameters.notify_replyto_rewrite.split(':')
2483
2484                if action in ['use_mail_domain']:
2485                    self.logger.debug('\t\t use_mail_domain:%s' %value)
2486                    tn.email2trac_replyto = '%s@%s' %(self.id, value)
2487
2488                elif action in ['use_trac_smtp_replyto']:
2489                    self.logger.debug('\t\t use_trac_smtp_replyto delimiter:%s' %value)
2490                    dummy = self.smtp_replyto.split('@')
2491                    if len(dummy) > 1:
2492                        tn.email2trac_replyto = '%s%s%s@%s' %(dummy[0], value, self.id, dummy[1])
2493                    else:
2494                        tn.email2trac_replyto = '%s%s%s' %(dummy[0], value, self.id)
2495
2496            if self.parameters.alternate_notify_template:
2497
2498                if self.VERSION >= 0.11:
2499
2500                    from trac.web.chrome import Chrome
2501
2502                    if  self.parameters.alternate_notify_template_update and not new:
2503                        tn.template_name = self.parameters.alternate_notify_template_update
2504                    else:
2505                        tn.template_name = self.parameters.alternate_notify_template
2506
2507                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2508                       
2509                else:
2510
2511                    tn.template_name = self.parameters.alternate_notify_template
2512
2513            tn.notify(tkt, new, modtime)
2514
2515        except Exception, e:
2516            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
2517
2518########## END Class Definition  ########################################################
2519
2520
2521########## Parse Config File  ###########################################################
2522
2523def ReadConfig(file, name):
2524    """
2525    Parse the config file
2526    """
2527    if not os.path.isfile(file):
2528        print 'File %s does not exist' %file
2529        sys.exit(1)
2530
2531    config = trac_config.Configuration(file)
2532   
2533    parentdir = config.get('DEFAULT', 'parentdir')
2534    sections = config.sections()
2535
2536    ## use some trac internals to get the defaults
2537    #
2538    tmp = config.parser.defaults()
2539    project =  SaraDict()
2540
2541    for option, value in tmp.items():
2542        try:
2543            project[option] = int(value)
2544        except ValueError:
2545            project[option] = value
2546
2547    if name:
2548        if name in sections:
2549            project =  SaraDict()
2550            for option, value in  config.options(name):
2551                try:
2552                    project[option] = int(value)
2553                except ValueError:
2554                    project[option] = value
2555
2556        elif not parentdir:
2557            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2558            print "or set parentdir in the [DEFAULT] section"
2559            sys.exit(1)
2560
2561    ## If parentdir then set project dir to parentdir + name
2562    #
2563    if not project.has_key('project'):
2564        if not parentdir:
2565            print "You must set project or parentdir in your configuration file"
2566            sys.exit(1)
2567        elif not name:
2568            print "You must configure a  project section in your configuration file"
2569        else:
2570            project['project'] = os.path.join(parentdir, name)
2571
2572    ##
2573    # Save the project name
2574    #
2575    project['project_name'] = os.path.basename(project['project'])
2576
2577    return project
2578
2579########## Setup Logging ###############################################################
2580
2581def setup_log(parameters, project_name, interactive=None):
2582    """
2583    Setup logging
2584
2585    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
2586    would be interpreted by the ConfigParser itself.
2587    """
2588    logger = logging.getLogger('email2trac %s' %project_name)
2589
2590    if interactive:
2591        parameters.log_type = 'stderr'
2592
2593    if not parameters.log_type:
2594        if sys.platform in ['win32', 'cygwin']:
2595            parameters.log_type = 'eventlog'
2596        else:
2597            parameters.log_type = 'syslog'
2598
2599    if parameters.log_type == 'file':
2600
2601        if not parameters.log_file:
2602            parameters.log_file = 'email2trac.log'
2603
2604        if not os.path.isabs(parameters.log_file):
2605            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
2606
2607        log_handler = logging.FileHandler(parameters.log_file)
2608
2609    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2610        ## Requires win32 extensions
2611        #
2612        logid = "email2trac"
2613        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
2614
2615    elif parameters.log_type in ('syslog', 'unix'):
2616        log_handler = logging.handlers.SysLogHandler('/dev/log')
2617
2618    elif parameters.log_type in ('stderr'):
2619        log_handler = logging.StreamHandler(sys.stderr)
2620
2621    else:
2622        log_handler = logging.handlers.BufferingHandler(0)
2623
2624    if parameters.log_format:
2625        parameters.log_format = parameters.log_format.replace('$(', '%(')
2626    else:
2627        parameters.log_format = '%(name)s: %(message)s'
2628        if parameters.log_type in ('file', 'stderr'):
2629            parameters.log_format = '%(asctime)s ' + parameters.log_format
2630
2631    log_formatter = logging.Formatter(parameters.log_format)
2632    log_handler.setFormatter(log_formatter)
2633    logger.addHandler(log_handler)
2634
2635    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2636        logger.setLevel(logging.DEBUG)
2637        parameters.debug = 1
2638
2639    elif parameters.log_level in ['INFO'] or parameters.verbose:
2640        logger.setLevel(logging.INFO)
2641
2642    elif parameters.log_level in ['WARNING']:
2643        logger.setLevel(logging.WARNING)
2644
2645    elif parameters.log_level in ['ERROR']:
2646        logger.setLevel(logging.ERROR)
2647
2648    elif parameters.log_level in ['CRITICAL']:
2649        logger.setLevel(logging.CRITICAL)
2650
2651    else:
2652        logger.setLevel(logging.INFO)
2653
2654    return logger
2655
2656########## Own TicketNotifyEmail class ###############################################################
2657
2658if __name__ == '__main__':
2659    ## Default config file
2660    #
2661    agilo = False
2662    configfile = '@email2trac_conf@'
2663    project = ''
2664    component = ''
2665    ticket_prefix = 'default'
2666    dry_run = None
2667    verbose = None
2668    debug_interactive = None
2669    virtualenv = '@virtualenv@'
2670
2671    SHORT_OPT = 'AcdE:hf:np:t:v'
2672    LONG_OPT  =  ['agilo', 'component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'virtualenv=', 'verbose']
2673
2674    try:
2675        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2676    except getopt.error,detail:
2677        print __doc__
2678        print detail
2679        sys.exit(1)
2680   
2681    project_name = None
2682    for opt,value in opts:
2683        if opt in [ '-h', '--help']:
2684            print __doc__
2685            sys.exit(0)
2686        elif opt in ['-A', '--agilo']:
2687            agilo = True
2688        elif opt in ['-c', '--component']:
2689            component = value
2690        elif opt in ['-d', '--debug']:
2691            debug_interactive = 1
2692        elif opt in ['-E', '--virtualenv']:
2693            virtualenv = value
2694        elif opt in ['-f', '--file']:
2695            configfile = value
2696        elif opt in ['-n', '--dry-run']:
2697            dry_run = True
2698        elif opt in ['-p', '--project']:
2699            project_name = value
2700        elif opt in ['-t', '--ticket_prefix']:
2701            ticket_prefix = value
2702        elif opt in ['-v', '--verbose']:
2703            verbose = True
2704
2705    if virtualenv and os.path.exists(virtualenv):
2706        activate_this = os.path.join(virtualenv, 'bin/activate_this.py')
2707        if os.path.exists(activate_this):
2708            execfile(activate_this, dict(__file__=activate_this))
2709
2710    try:
2711        from trac import __version__ as trac_version
2712        from trac import config as trac_config
2713
2714    except ImportError, detail:
2715        print "Can not find a a valid trac installation, solutions could be:"
2716        print "\tset PYTHONPATH"
2717        print "\tuse the --virtualenv <dir> option"
2718        sys.exit(1)
2719   
2720    settings = ReadConfig(configfile, project_name)
2721
2722    ## The default prefix for ticket values in email2trac.conf
2723    #
2724    settings.ticket_prefix = ticket_prefix
2725    settings.dry_run = dry_run
2726    settings.verbose = verbose
2727
2728    if not settings.debug and debug_interactive:
2729        settings.debug = debug_interactive
2730
2731    if not settings.project:
2732        print __doc__
2733        print 'No Trac project is defined in the email2trac config file.'
2734        sys.exit(1)
2735
2736    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2737   
2738    if component:
2739        settings['component'] = component
2740
2741    ## We are only interested in the major versions
2742    # 0.12.3 --> 0.12
2743    # 1.0.2  --> 1.0
2744    #
2745    l = trac_version.split('.')
2746    version = '.'.join(l[0:2])
2747
2748    logger.debug("Found trac version: %s" %(version))
2749   
2750    try:
2751        if version == '0.10':
2752            from trac import attachment
2753            from trac.env import Environment
2754            from trac.ticket import Ticket
2755            from trac.web.href import Href
2756            from trac import util
2757            from trac.ticket.web_ui import TicketModule
2758
2759            #
2760            # return  util.text.to_unicode(str)
2761            #
2762            # see http://projects.edgewall.com/trac/changeset/2799
2763            from trac.ticket.notification import TicketNotifyEmail
2764            from trac import config as trac_config
2765            from trac.core import TracError
2766
2767        elif version in ['0.11', '0.12', '0.13', '1.0', '1.1']:
2768            from trac import attachment
2769            from trac import config as trac_config
2770            from trac import util
2771            from trac.core import TracError
2772            from trac.env import Environment
2773            from trac.perm import PermissionSystem
2774            from trac.perm import PermissionCache
2775            from trac.test import Mock, MockPerm
2776            from trac.ticket.api import TicketSystem
2777            from trac.ticket.web_ui import TicketModule
2778            from trac.web.href import Href
2779
2780            if agilo:
2781
2782                try:
2783                    #from agilo.utils.config import AgiloConfig
2784                    #if AgiloConfig(self.env).is_agilo_enabled:
2785                    from agilo.ticket.model import Ticket
2786                except ImportError, detail:
2787                    logger.error('Could not find Trac  Agilo environemnt')
2788                    sys.exit(0)
2789
2790            else:
2791
2792                from trac.ticket import Ticket
2793
2794            #
2795            # return  util.text.to_unicode(str)
2796            #
2797            # see http://projects.edgewall.com/trac/changeset/2799
2798            from trac.ticket.notification import TicketNotifyEmail
2799            from trac.notification import NotifyEmail
2800
2801        else:
2802            logger.error('TRAC version %s is not supported' %version)
2803            sys.exit(0)
2804
2805        ## Must be set before environment is created
2806        #
2807        if settings.has_key('python_egg_cache'):
2808            python_egg_cache = str(settings['python_egg_cache'])
2809            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2810
2811        if settings.debug > 0:
2812            logger.debug('Loading environment %s', settings.project)
2813
2814        try:
2815            env = Environment(settings['project'], create=0)
2816        except IOError, detail:
2817            logger.error("trac error: %s" %detail)
2818            sys.exit(0)
2819        except TracError, detail:
2820            logger.error("trac error: %s" %detail)
2821            sys.exit(0)
2822
2823        tktparser = TicketEmailParser(env, settings, logger, float(version))
2824        tktparser.parse(sys.stdin)
2825
2826    ## Catch all errors and use the logging module
2827    #
2828    except Exception, error:
2829
2830        etype, evalue, etb = sys.exc_info()
2831        for e in traceback.format_exception(etype, evalue, etb):
2832            logger.critical(e)
2833
2834        if m:
2835            tktparser.save_email_for_debug(m, settings.project_name, True)
2836
2837        sys.exit(1)
2838
2839# EOB
Note: See TracBrowser for help on using the repository browser.