source: trunk/email2trac.py.in @ 631

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

We can now support name+id@… addresses:

  • recipient_delimiter: +

wrote a simple notication function that sets the reply to field of a mail to: name+id@…

  • notification_replyto_rewrite: 1

For each bug there will be an uniqye address

Still some work to be done:

see #297

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