source: trunk/email2trac.py.in @ 639

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

deleted obsoleted lines

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